Named secrets¶
Named secrets inject credentials into a project's agent containers as ordinary environment variables, scoped to the projects that are allowed to use them. They keep API keys and tokens out of project documents and workflow definitions — the agent just reads an environment variable.
Declaring secrets¶
Add a named_secrets list to the daemon configuration:
named_secrets:
- name: WEATHER_API_KEY
value: "${WEATHER_API_KEY}" # resolved from the daemon's environment
allowed_projects:
- forecast-bot
- name: SHARED_ANALYTICS_TOKEN
value: "${ANALYTICS_TOKEN}"
allowed_projects: [] # empty = available to every project
| Field | Meaning |
|---|---|
name |
The environment-variable name the agent will see. |
value |
The secret value. Supports ${VAR} expansion (below). |
allowed_projects |
Projects whose agents receive this secret. Empty = all projects. |
Keep the literal value out of the file¶
value is expanded with ${VAR} syntax against the daemon's own
environment at startup, so you don't have to write the raw secret into the
config file. Store the real value where the daemon process reads its
environment — for example a systemd EnvironmentFile or your secret manager —
and reference it:
- name: WEATHER_API_KEY
value: "${WEATHER_API_KEY}" # the real key lives in the daemon's env
Expansion happens once, when the configuration is loaded. Update the environment and reload the daemon to pick up a rotated value.
Where the env files are read from¶
Before expanding placeholders, vornik sources these files itself, so ${VAR}
resolves the same way for the daemon and for vornikctl — which has no systemd
EnvironmentFile of its own:
<config-dir>/vornik.env— the file the podman quickstart seeds<config-dir>/secrets/env<config-dir>/secrets/*.env— one file per service, alphabetical
A variable already present and non-empty in the real environment always wins, so
an explicit export or a systemd EnvironmentFile still overrides these files.
Earlier entries in the list win over later ones.
If a required setting comes out empty because its placeholder had nothing to resolve to, the error names the variables involved:
configuration validation failed: database name is required (unset config
placeholders: ${POSTGRES_DB}, ${POSTGRES_USER} — set them in
<configDir>/vornik.env or <configDir>/secrets/*.env, or export them before
running)
Scoping with allowed_projects¶
allowed_projects controls which projects' agents receive each secret:
- Empty list → every project's agents get the variable.
- Non-empty list → only the named projects' agents get it; agents in any other project never see the variable at all.
Scope every secret as narrowly as the work requires — a project that doesn't need a credential should never receive it.
How the agent sees it¶
For an allowed project, the secret is injected into the agent container as an
environment variable named exactly by name. An agent reads it the usual way
(for example, os.Getenv("WEATHER_API_KEY")). When the same variable name is
set in more than one place, per-task environment overrides a named secret,
which overrides a role's static environment.
Named secrets are configured in the daemon configuration only — there is no separate command-line surface for managing them.
Carrying tool-issued credentials to you¶
Named secrets push credentials into an agent. The opposite problem is a
credential that comes out of a tool: some tools mint an access credential for
the artifact they produce — canonically PageDrop, whose publish tools return a
viewing password for the page. You need that password, but the outbound
secret-redactor (which scrubs agent chat replies and notifications) treats a
password-shaped value like any other secret and replaces it with
[REDACTED:…] before it reaches you.
Credential carryover closes that gap. The daemon captures the credential deterministically from the trusted tool's own output — no model decides what's a credential — stores it against the task, and surfaces it code-formatted and one-tap-copyable in the task's completion notification (Telegram) and on the task-detail page, instead of redacting it.
Enabling it¶
Add a tool_credentials mapping under secrets. Each entry names a tool (by
prefix) and how to extract the credential from its output — either a text
pattern (a regexp with one capture group, for tools that return prose) or a
JSON field (a dotted path, for tools that return a JSON object):
secrets:
# A capturing tool is implicitly trusted-output for its captured field, but
# listing it here too keeps the value un-redacted in the tool-audit log.
trusted_output_tools:
- "mcp__pagedrop__pagedrop_publish"
tool_credentials:
# PageDrop returns prose ("View: <url> … Password: <pw>"), so use patterns:
- tool: "mcp__pagedrop__pagedrop_" # prefix — matches publish/republish
credential_pattern: "Password:\\s*(\\S+)"
artifact_pattern: "View:\\s*(\\S+)" # optional; links the credential to a URL
label: "viewing password" # operator-facing name
# A tool that returns JSON would use field paths instead:
# - tool: "mcp__example__publish"
# credential_field: "data.password" # dotted path; no array indexing
# artifact_field: "url"
# label: "access token"
| Key | Meaning |
|---|---|
tool |
Tool-name prefix whose output carries the credential. |
credential_pattern |
Regexp (one capture group) extracting the credential from text output. Takes precedence over credential_field. |
artifact_pattern |
Optional regexp (one capture group) for the URL the credential unlocks. |
credential_field |
Dotted path into a JSON result to the credential (e.g. data.password). No array indexing. |
artifact_field |
Optional dotted path to the artifact URL. |
label |
Operator-facing name shown next to the value (defaults to credential). |
With no tool_credentials entries (the default) nothing is captured and
behaviour is unchanged.
What it does and doesn't do¶
- The model is never involved in deciding what's a credential — capture is driven by the operator-configured tool + field/pattern, reading the tool's daemon-proxied output that an agent cannot forge.
- Strong keys are never captured. A value matching a strong, prefix-anchored pattern (OpenAI / Anthropic / GitHub / AWS / JWT / …) is refused even from a configured tool and always redacts — only the lower-confidence "viewing-password" shape is eligible. So a misconfigured mapping can't turn a real API key into a carryover, and it isn't an exfiltration channel.
- Retries don't leak stale credentials. Only the task's most recent execution's credential is surfaced; an earlier attempt's password is hidden.
- The value is stored alongside the task for its lifetime (same as the tool-audit row that already holds it) and removed when the task is purged.