The vault
Every credential a workspace holds lives in one place, encrypted, and no route ever returns one — not even masked. An agent never receives a secret. It receives a capability.
Write-only, and what that means
The vault is in Settings, under Vault. You can write a secret and you can replace it. You cannot read one back, and neither can anyone else, including an administrator.
This is structural rather than a policy:
- No route returns a value, in full or masked.
- The list route does not decrypt and does not even select the secret columns. It asks the database for two booleans — is it configured, is it a legacy plaintext row — so there is nothing present to redact.
- Values are encrypted at rest, always. Any legacy plaintext row is re-encrypted when the server starts.
- The realtime layer strips both the value and the cipher columns as a third layer of defence.
Losing a secret means rotating it at the provider and writing the new one. That is the intended workflow.
Four namespaces
| Group | What it holds | Written from |
|---|---|---|
| Platform | Managed keys, such as the workspace's own model API key | Settings → AI |
| Provider | A provider skill's API key | The provider credentials route (manage role) |
| Orb | A webhook's signing secret | The orb's own panel |
| Shared | Anything else the team needs to share | The vault's own write route |
A single classifier decides which group a key is in and which lane may write it, and both backends use that same classifier — so a secret written through one is understood identically by the other. A malformed namespaced key is reported as an orphan rather than guessed at, so nothing offers to overwrite the wrong row.
How an agent uses a credential without seeing it
This is the part worth understanding properly. An agent that needs to call a provider API does not get the key. It calls call_provider and names four things, and only four:
skill_id | Which provider skill |
operation | Which named operation on it |
path_params | Values for the operation's path placeholders |
body | The request body |
The server resolves the base URL and the endpoint path from the skill definition, attaches the vault credential, makes the request, and returns the response fenced as untrusted data.
The rules that make this a security feature
- Anything else is refused by name. A
url,host,headersorauthorizationargument is a rejection, not a silently dropped key. - Path params are the only caller input that reaches the URL, restricted to a charset that cannot escape its path segment. The resolved URL is then re-checked against the base origin.
- Redirects are refused, not followed. A public host redirecting to another public host would pass every per-hop check and then be handed the Authorization header, so the redirect is simply not taken.
- Outbound addresses are checked against the same guard the rest of the system uses — one implementation, not a second one that could drift. Address comparison is numeric, so an IPv6 spelling of an internal address does not slip through a string test.
- Only one shape is allowed out of a call, into both the tool result and the audit record. It has no field that could hold a secret — absent, not redacted. Exactly one place in the code attaches a credential to a request, and a test asserts there is exactly one.
- Scoping. Only agents may call it, and only for a skill that the calling agent carries. The workspace and agent come from the token, so a transient join secret cannot spend a provider key.
- Rate limited per agent, at 20 calls a minute, on top of the general limit.
Every call is audited
One Activity row per call: provider, operation, method, resolved URL, status, duration. Never a body, never a header, never the vault key's name.
The vault beats the environment
When resolving a credential, the vault is read first. A host environment variable is a fallback for a locally-run server — and crucially, the name of that variable comes from the bundled skill definition, never from an agent-authored one. Otherwise an agent able to write its own metadata could name the server's own auth secret and have it attached as a bearer token.
Reaching it
The vault table is not in the generic database allowlists. The dedicated manage-role routes are the only doors, and the general write route's key charset excludes the colon, so it cannot address a namespaced entry belonging to another lane.
If you run agensis across two hosts, the encryption key must be identical on both — a secret written on one is otherwise undecryptable on the other. See Self-hosting.