rec 001 capability control plane for ai agents

Your agents never
touch a credential.

Toolgate sits between agents and their tools. Agents hold one Ed25519 key — nothing else. Every call is policy-checked, budget-metered, executed with credentials injected server-side, and sealed into a signed audit chain.

≤300s
token lifetime, jittered
0
secrets held by agents
100%
decisions audited, denials included
toolgate demo --live

    

every call, in order

  1. verifytoken + one-time proof
  2. decidebounds → policy → default deny
  3. budgetatomic charge
  4. injectcredential from vault
  5. executeupstream call
  6. auditsigned, hash-chained

rec 002 interactive · runs entirely in your browser

Send a call through the gate.

This is the real decision logic and a real SHA-256 hash chain, computed live with Web Crypto. Pick what the agent tries to do — then try to tamper with the record of it.

agent request

delegation budget

8 / 8 units

active policy · first match wins

  1. deny crm · delete_*
  2. approval email · send_email · to ∉ acme.com
  3. allow email · send_email
  4. allow crm · *
  5. deny anything else

gate pipeline

  1. verify
  2. token bounds
  3. policy
  4. budget
  5. inject + execute

audit chain · sha-256, each record seals the one before it

— no records yet. send a call through the gate. —

rec 003 integrate

Delegate. Call. Approve.
Three moves, fully audited.

01 The human delegates bounded authority
# POST /v1/control/grants
{
  "userId": "usr_sam", "agentId": "agt_assistant",
  "authorization": [{"upstream": "crm", "tools": ["*"]}],
  "budgetMaxUnits": 100, "policyId": "pol_default"
}
02 The agent calls tools — with zero secrets
# pip install toolgate
client = ToolgateClient(
    agent_private_jwk=key,  # its only secret
    grant_id="grt_...", base_url=GATE)

r = client.call("crm", "read_contact",
                {"contactId": "c-014"})
03 Risky calls park for a human
parked = client.call("email", "send_email",
                     {"to": "cfo@globex.com", ...})
# 202 pending_approval — Sam sees the exact args
done = client.wait_for_approval(parked.approval_id)

rec 004 token anatomy

Delegation, not impersonation.

RFC 8693 semantics: sub stays the human, act.sub is the agent. Hover a claim.

{
  "sub": "usr_sam",
  "act": { "sub": "agt_assistant" },
  "aud": "toolgate:gate",
  "authorization_details": [
    { "upstream": "crm", "tools": ["*"] }
  ],
  "cnf": { "jkt": "kUx0...Q7w" },
  "txn": "txn_9f21",
  "exp": 1788427121   // ~120s, jittered
}

sub — the human whose authority is exercised. Never the agent.

act.sub — who is actually acting. Audit records carry both, forever distinguishable.

aud — audience-bound; rejected anywhere but the gate. No token passthrough.

authorization_details — the reachable surface (RFC 9396). Outside it, policy never even runs: denied.

cnf.jkt — thumbprint of the agent key that must sign a one-time proof per call. A stolen token is inert.

txn — per-task id; the join key across every audit record it produced.

exp — minutes, not months. Jittered so harvested batches never expire together.

new in 0.5

  1. oauth brokeringagents call with the user's own account
  2. push approvalssigned webhooks · Slack · magic links
  3. proof-grade audittransparency-log anchoring + WORM
  4. kms vaultenvelope encryption, keys never leave KMS
  5. postgresN instances, exactly-once guarantees

rec 005 guarantees

What an attacker gets.

Steals the token

Nothing. Every call needs a fresh proof signed by the agent key named in cnf.jkt. Replays are single-use-jti dead.

TG_PROOF_INVALID

Steals token + key

A bounded box: authorized tools ∩ policy ∩ remaining budget ∩ ≤300s. And revocation kills live tokens on the next call.

TG_REVOKED

Rewrites history

Every decision — denials included — is hash-chained and Ed25519-signed. Edit, remove, or reorder one record and verification names the exact breakpoint.

valid: false, broken_at_seq: n