Admin API (runtime config-write)

The admin API lets a control plane (e.g. govder) configure the enforcement plane at runtime — push policies, mint/revoke tokens, manage roles, and register credentials — without restarting vultrino. It is served by vultrino web under /api/v1/ alongside the existing read/execute endpoints.

Authentication

Every admin endpoint requires an API key (vk_…) whose role holds the admin permission. The predefined admin role has it; grant it to a custom role with "permissions": ["admin", …]. Use tokens (vut_…) are rejected outright — admin is an API-key-only capability.

Authorization: Bearer vk_your_admin_key

Responses: 401 (missing/invalid key), 403 (valid key without admin, or a use token), 400 (invalid body), 404 (no such resource), 409 (duplicate or an in-flight idempotency key), 201/200 on success.

Idempotency

Mutating endpoints accept an optional Idempotency-Key header. While the first request is still in flight, a second with the same key gets 409. Keys are remembered for 24h.

Idempotency-Key: 5f3c…unique-per-logical-request

The key is bound to a hash of the request body: reusing a key with a different body returns 409 rather than replaying the original response.

A repeat with the same key and body is handled per route class:

  • Creates and mints (POST /policies, POST /capabilities, POST /tokens, POST /approval-tokens, POST /roles, POST /credentials) replay the original response instead of acting again, so a retried token mint never creates a second token. Minted token plaintext is not retained in the idempotency record — a replayed mint returns metadata plus a note (revoke and re-mint if you lost the original response).
  • Convergent writes (PUT /policies/{id}, PUT /capabilities/{id}, PUT /roles/{name}, POST /agents/{label}/halt) re-apply the body and return the fresh result. These address a caller-supplied deterministic id, so a second application only converges — and short-circuiting them would let a content-derived key silently leave an in-between (possibly wider) version enforced while reporting success. Each applied repeat emits its own policy.changed/capability.changed outbox event.

At-least-once on crash. Reserve → operate → record-completion are three separate atomic storage writes, not one transaction. If the process crashes after the operation persists but before completion is recorded, a retry (after the ~60s stale-reservation window) re-runs the operation. Idempotency is exactly-once only absent a mid-operation crash.

Endpoints

Policies

Policies pushed here are merged with the static [[policies]] from config.toml into the live engine (config policies stay declarative; the API manages dynamic ones by id). Each write hot-reloads the engine on the web process synchronously.

Cross-process propagation. Other long-running processes that share the same vault (notably the MCP server) reload policies on a periodic refresh (default 5s), so an admin push reaches them within that window — not instantly. For an immediate kill, revoke the use token instead: token revocation is storage-authoritative and re-checked under the lock on every gated call, so it takes effect on the very next call in every process.

MethodPathBodyResult
POST/api/v1/policies{name, credential_pattern, rules?, default_action, id?}201 canonical policy (id generated if omitted)
PUT/api/v1/policies/{id}same200 canonical policy (create-or-replace)
DELETE/api/v1/policies/{id}—200 {deleted} / 404

rules and default_action use the same shape as the config file (allow / deny / prompt). An invalid credential_pattern glob is rejected with 400 rather than silently never matching.

Use tokens

MethodPathBodyResult
POST/api/v1/tokens{name, credential_scope, action_scope?, max_uses?, require_approval?, expires_in_secs?}201 {token, metadata} — plaintext shown once
POST/api/v1/tokens/{id}/revoke—200 {revoked, metadata} / 404

Roles

MethodPathBodyResult
POST/api/v1/roles{name, permissions[], credential_scopes?, description?}201 role / 409 if the name exists
DELETE/api/v1/roles/{id}—200 {deleted} / 404

Credentials

Secret material is write-only: it is stored encrypted and never returned by any endpoint (the create response carries metadata only).

MethodPathBodyResult
POST/api/v1/credentials{alias, metadata?, data}201 credential metadata / 409 duplicate alias
DELETE/api/v1/credentials/{id}—200 {deleted} / 404

data is the tagged credential payload, e.g. {"type":"api_key","key":"…","header_name":"Authorization","header_prefix":"Bearer "}.

Webhooks

There is no PUT /api/v1/config/webhooks route in this build. Signed event delivery is configured in config.toml under [outbox] (url, hmac_secret); see Event Outbox.

Deployment note (vault format)

The admin API's stored policies and idempotency records live in the encrypted vault, whose on-disk format is versioned (STORAGE_VERSION, currently 10). A newer binary migrates an older (v9 or earlier) vault one way when it first opens it, keeping a backup, after which any still-running older binary (a not-yet-upgraded MCP or CLI process sharing the same vault) is refused the vault entirely. Stop and upgrade all vultrino processes together, so no older binary is still running when the new one first opens the vault, to avoid breaking the un-upgraded enforcement plane.

Example

# Push an allow policy for github credentials (takes effect immediately).
curl -sX POST http://127.0.0.1:7879/api/v1/policies \
  -H "Authorization: Bearer $VULTRINO_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"gh-allow","credential_pattern":"github-*","default_action":"allow"}'