Token Vault
Manage

API Keys & Scopes

Admin API keys and scoped agents for CLI, CI, and automation, with least-privilege scopes and no-escalation delegation.

Not everything that talks to Token Vault is a person at a console. Scripts, CI jobs, and automation need their own identity, with only the permissions they need. Token Vault gives you three non-human principals, all sent as Authorization: Bearer.

PrincipalKey prefixPowers
HumanFirebase sign-in (console or tvault login)Every scope, plus the human-only actions below
API keytvkey_…The scopes you pick when you create it. Built for CLI, CI, and automation
Classic agenttvagent_…credentials:read + mcp:use, bounded by its grants. Unchanged
Scoped agenttvagent_…An agent created with a scope list, so it can do more than read credentials

Credentials never pass through Token Vault

Scopes govern the control plane. When a key writes a credential with tokens:create, the value goes straight from the CLI to your webhook on a signed store ticket. Token Vault never sees it.

Create an API key

tvault keys create --name ci --scopes tokens:create-read,agents:create --expires 90d
# tvkey_...   <- printed to stdout once
# Same thing over HTTP (human sign-in within the last 5 minutes required)
curl -X POST https://api.tokenvault.uk/api/keys \
  -H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci","scopes":["tokens:create-read","agents:create"],"expiresAt":"2027-01-05T00:00:00Z"}'

Shown once

The tvkey_… value is returned a single time and is never retrievable. If you lose it, rotate the key (tvault keys rotate <id>, as a signed-in human) and update wherever it was stored. A key that is still working can rotate itself with tvault keys rotate --self.

Expiry is 30, 90, or 365 days, a custom date, or never. --expires 90d, --expires 2027-01-05, or --expires never. Prefer a short expiry for anything that runs in CI.

Manage keys

tvault keys ls                          # name, scopes, status, expiry, last used
tvault keys show <id>                   # one key, plus its grants
tvault keys rotate <id>                 # new tvkey_ value; the old one stops working
tvault keys revoke <id>                 # revoke this key only (needs keys:revoke); keys it created are not affected
tvault keys grant <id> github           # give the key read access to a token
tvault keys ungrant <id> github

Check who you are at any time:

tvault whoami      # principal, kind, scopes, expiry

Scoped agents

An agent is a classic agent unless you give it scopes. A scoped agent keeps its tvagent_… key and grants, and gains control-plane powers on top.

tvault agents create --name hub-minter --kind scoped --scopes tokens:create
tvault agents rotate-key hub-minter     # lost the key? rotate it

See the scope reference for all 18 scopes.

Every key stands alone

A key or scoped agent that can create other principals can only hand down what it has, and only at the moment it creates them.

  • No escalation at creation. A child gets a subset of the creator's scopes and grants. A child key's expiry is at most the creator's expiry at that moment.
  • Some scopes only a human can hand out. keys:manage, keys:revoke, agents:manage, agents:delete, policies:write, grants:write, tokens:update, tokens:delete, and proxies:write can only be put on a key or agent by a signed-in human, in the console or CLI. A key that tries gets 403 SCOPE_NOT_DELEGABLE, even if it holds the scope itself.
  • Independent afterwards. Once created, a key or agent is decided by its own record only: its own status, expiry, scopes, grants, and attached policies. Revoking, suspending, deleting, narrowing scopes, removing grants, or attaching a policy or rate limit to one key never affects another, including keys it created.
  • Revoking a key does not revoke what it created. If a key is compromised, revoke it, then use the "Created by" column to find the keys and agents it made and revoke those yourself.
  • Scopes cover the whole account. A scope means the same thing whoever holds it. agents:manage works on any agent, tokens:delete on any token, whether or not that key created it. "Created by" is information only and grants no reach.
  • Delete is separate from manage. agents:manage edits, suspends and resumes agents. agents:delete deletes them. keys:manage creates, lists, edits, suspends and resumes keys. keys:revoke revokes them.
  • Suspend and resume are symmetric. Anything with agents:manage or keys:manage can resume another principal, including one a human suspended. It can never resume itself.
  • A key or agent can't loosen its own controls. It can't change its own scopes, turn its own MCP back on, resume itself, edit, detach or delete policies attached to itself, or change its own grants (403 SELF_CHANGE_FORBIDDEN). It can rotate its own key (--self), suspend itself, rename itself, and add a policy to itself. A human can do all of these.
  • Narrowing is always fine. The SCOPE_NOT_DELEGABLE check applies only to human-grant-only scopes being newly added. Removing scopes from a key that already has them is allowed.
  • Proxies. Changing a webhook proxy's upstream URL, headers or service needs credentials:read and a grant on that service, the same as creating one. Changing a session (tv_session) proxy's upstream is human-only. Only a human, or a scoped agent binding itself, can create a session proxy.
  • Auto read grants can be passed on, but stay tied to one creation of the token. The read grant a key gets from tokens:create-read can be passed on with grants:write (copies of copies too) and used to bind a proxy. Every token creation gets a new creation id: a fresh create, a key re-claiming its own name with a new ticket, and every take-over. The grant, every copy, and every proxy bound from one work only while the token is still on the creation they were made under. Re-creating, overwriting, OAuth-connecting or deleting the token, or binding or rebinding the webhook, lapses them, and once lapsed they never come back, even if the same key becomes the creator again. The one write that keeps them alive is the creating key or agent overwriting its own token. If a webhook bind or rebind can't reset them, the bind is refused (503 TOKEN_OWNERSHIP_UNAVAILABLE). Without a live grant of its own, a key gets 403 GRANT_REQUIRED when it tries to pass a token on or bind a proxy.
  • Auto and regular grants don't replace each other silently. A key or agent can't replace a target's regular grant with an auto-derived copy (409 MANUAL_GRANT_IN_PLACE), or the reverse (409 AUTO_GRANT_IN_PLACE). A human can replace either.
  • Limits. Token Vault can't see a value changed directly at your webhook, so that doesn't lapse anything. A credential ticket already minted (valid 60 seconds) can still be redeemed after the value changes.
  • Editing a proxy as a human clears its tie. When you change a proxy's upstream URL, headers or service, it no longer lapses if the token's creator changes.
  • grants:write also needs credentials:read. A key can only hand out access to tokens it can read. A grant a key passes on can't outlive the key's own grant.
  • Changing a proxy's upstream URL needs new headers. A key that changes the upstream URL must send the headers in the same edit (400 HEADERS_REQUIRED).
  • Deleting a token removes every grant on it. A key can't re-create a token name that still has proxies bound to it (409 TOKEN_OWNED).
  • Folder grants. A proxy bound from a folder grant is gated by that folder's policies.
  • Policies copied at creation. A key or agent created by a key starts with copies of its creator's directly attached policies. This happens at creation only; later changes to either side don't propagate.
  • Self-change covers proxies. "Can't loosen its own controls" includes policies on proxies the key created.
  • Dry runs are free. POST /api/policies/evaluate never consumes rate-limit or usage counters and writes no audit events, for anyone with policies:read.
  • Rotating keys. Any key or agent can rotate its own key with no extra scope: tvault keys rotate --self or tvault agents rotate-key --self (the CLI switches your stored context to the new key and prints it once). Rotating or revealing another principal's key is human-only (403 HUMAN_ONLY). If two rotations race, the loser gets 409 ROTATION_CONFLICT and no new key is issued.
# ci can mint a child key, but only with scopes ci itself holds
tvault keys create --name ci-child --scopes tokens:create --expires 30d

Human-only actions

No key, of any scope, can do these. They require a signed-in human.

ActionWhy
Unlock the vaultA person decides when automation may run
Bind or rebind the webhookMoves the data plane
Delete the vaultIrreversible
Add MCP redirect hostsChanges where OAuth tokens can be sent
Delete or export the accountAccount-level
Rotate or reveal another principal's keyA key may only rotate its own
Grant the scopes listed above to a key or agentA person decides who gets destructive powers

A key that tries gets 403 HUMAN_ONLY.

Locked vault freezes every key

When the vault is locked, every API key and scoped agent stops working until a human unlocks it. This is the same kill switch as Stop All Access.

Policies

Attach access policies to an API key with entityType: "api_key":

curl -X POST https://api.tokenvault.uk/api/policies/$POLICY_ID/attach \
  -H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"attachments":[{"entityType":"api_key","entityId":"'"$KEY_ID"'"}]}'

Detach uses the same body shape at /api/policies/$POLICY_ID/detach.

Upgrading

  • Deleting a token now removes every grant on it and drops it from folders, so re-creating the same name does not restore anyone's access. Grant access again after re-creating.
  • Token delete needs Firestore to clean up, so it returns 503 if Firestore is unavailable. Nothing is deleted; retry.
  • Rebinding the webhook lapses every tokens:create-read grant (and copies and proxies bound from them). Grant access again after a rebind.

Worked example: store a Cloudflare token from a script

A script on your mcp-hub mints a Cloudflare service token and saves it. It needs tokens:create and nothing else, so it can write a token but never read one back.

# One-time, as a human
tvault agents create --name cf-minter --kind scoped --scopes tokens:create
# tvagent_...  <- shown once; store it in the hub's secret store

# In the hub script
printf %s "$CF_MINTER_KEY" | tvault login --key-stdin --as cf-minter
CF_TOKEN=$(./mint-cloudflare-service-token.sh)     # your minting step
tvault tokens create --service cloudflare-zt --value "$CF_TOKEN"

The value goes from the CLI to your webhook on a signed store ticket. If you want the script to read it back immediately, give it tokens:create-read (which grants the creator a read grant at creation time) and credentials:read (which is what allows the read itself).

Pipe keys in with --key-stdin rather than passing --key on the command line, where it would land in shell history and process listings.

Errors

Errors return {"detail":{"code","message","missingScope"}}. See scopes, errors and exit codes.

On this page