Capability Tokens (IBCT)
Capability Tokens (IBCT)
Invocation-Bound Capability Tokens (IBCTs) fuse identity, authorization, and provenance into a single token that an agent presents when performing an action. IBCTs are the runtime authorization mechanism for delegated agent actions.
Why IBCTs?
Traditional access control separates identity (who are you?) from authorization (what can you do?). For AI agents operating across multiple services, this separation creates gaps:
| Problem | IBCT Solution |
|---|---|
| Bearer tokens can be stolen | IBCT is bound to the agent’s DID key pair |
| No delegation provenance | IBCT chain traces back to human authorization |
| Static permissions | IBCT encodes exact scope for each invocation |
| No audit trail | Each IBCT is logged with cryptographic proof |
Token Formats
IDA supports two IBCT formats depending on the delegation depth:
| Format | Use Case | Structure | Library |
|---|---|---|---|
| Compact JWT | Single-hop delegation (human -> agent) | EdDSA-signed JWT | jose / jsonwebtoken |
| Chained Biscuit | Multi-hop delegation (human -> agent -> sub-agent) | Append-only token with Datalog | biscuit-auth |
Compact JWT (Single-Hop)
For simple human-to-agent delegations, IBCTs use a compact EdDSA-signed JWT:
Token Structure
Header.Payload.Signature
Header
1 { 2 "alg": "EdDSA", 3 "typ": "IBCT+jwt", 4 "kid": "did:adi:agent:shop01...#key-1" 5 }
Payload
1 { 2 "iss": "did:adi:agent:shop01...", 3 "sub": "did:adi:agent:shop01...", 4 "aud": "https://freshmart.com/api", 5 "iat": 1711612800, 6 "exp": 1711616400, 7 "nbf": 1711612800, 8 "jti": "ibct-f47ac10b-58cc-4372", 9 10 "delegator": "did:adi:human001...", 11 "delegation_vc": "urn:uuid:root-delegation", 12 "scope": ["purchase-groceries"], 13 "constraints": { 14 "maxSpendPerTransaction": 50, 15 "currency": "USD" 16 }, 17 18 "action": { 19 "type": "purchase", 20 "resource": "/api/v1/orders", 21 "method": "POST" 22 }, 23 24 "provenance": { 25 "chain": ["did:adi:human001...", "did:adi:agent:shop01..."], 26 "rootDelegation": "urn:uuid:root-delegation" 27 } 28 }
Creating a Compact IBCT
1 import { SignJWT } from 'jose'; 2 import { importJWK } from 'jose'; 3 4 async function createCompactIBCT(agentPrivateKey, delegationVC, action) { 5 const jwt = await new SignJWT({ 6 delegator: delegationVC.credentialSubject.delegator, 7 delegation_vc: delegationVC.id, 8 scope: delegationVC.credentialSubject.scope, 9 constraints: delegationVC.credentialSubject.constraints, 10 action: { 11 type: action.type, 12 resource: action.resource, 13 method: action.method, 14 }, 15 provenance: { 16 chain: [delegationVC.credentialSubject.delegator, delegationVC.credentialSubject.delegate], 17 rootDelegation: delegationVC.id, 18 }, 19 }) 20 .setProtectedHeader({ 21 alg: 'EdDSA', 22 typ: 'IBCT+jwt', 23 kid: agentKeyId, 24 }) 25 .setIssuer(agentDid) 26 .setSubject(agentDid) 27 .setAudience(targetService) 28 .setIssuedAt() 29 .setExpirationTime('1h') 30 .setJti(`ibct-${crypto.randomUUID()}`) 31 .sign(agentPrivateKey); 32 33 return jwt; 34 }
Verifying a Compact IBCT
1 import { jwtVerify } from 'jose'; 2 3 async function verifyCompactIBCT(token, resolverService) { 4 // 1. Decode header to get agent DID and key ID 5 const { kid } = decodeProtectedHeader(token); 6 const agentDid = kid.split('#')[0]; 7 8 // 2. Resolve agent DID Document 9 const didDoc = await resolverService.resolve(agentDid); 10 11 // 3. Extract public key 12 const verificationMethod = didDoc.verificationMethod.find(vm => vm.id === kid); 13 const publicKey = importPublicKey(verificationMethod); 14 15 // 4. Verify JWT signature 16 const { payload } = await jwtVerify(token, publicKey, { 17 issuer: agentDid, 18 audience: expectedAudience, 19 }); 20 21 // 5. Verify delegation chain 22 const chainValid = await resolverService.verifyDelegationChain( 23 payload.delegation_vc, 24 payload.delegator, 25 agentDid 26 ); 27 28 // 6. Verify scope covers the requested action 29 const scopeValid = checkScopeCoversAction(payload.scope, payload.action); 30 31 return { valid: chainValid && scopeValid, payload }; 32 }
Chained Biscuit (Multi-Hop)
For multi-hop delegation chains, IBCTs use Biscuit tokens with Datalog authorization policies.
Why Biscuit?
Biscuit tokens are append-only: each delegation hop adds a block to the token, narrowing the scope. The original authority block cannot be modified by downstream delegates.
Token Structure
Authority Block (from root delegator) + Attenuation Block 1 (from first agent) + Attenuation Block 2 (from sub-agent) + Signature Chain
Authority Block (Root)
1 // Authority: Human delegates to Agent 2 right("did:adi:agent:shop01", "purchase-groceries"); 3 right("did:adi:agent:shop01", "compare-prices"); 4 check if time($time), $time < 2026-09-15T00:00:00Z; 5 check if spend($amount), $amount <= 200; 6 check if merchant($m), ["FreshMart", "OrganicCo"].contains($m);
Attenuation Block (Agent -> Sub-Agent)
1 // Attenuation: Agent delegates narrower scope to Sub-Agent 2 check if right("did:adi:agent:price01", "compare-prices"); 3 // Removed: purchase-groceries (scope narrowed) 4 // Added: read-only constraint 5 check if method($m), $m == "GET"; 6 check if time($time), $time < 2026-06-15T00:00:00Z;
Creating a Chained Biscuit
1 use biscuit_auth::{Biscuit, KeyPair, builder::*}; 2 3 fn create_authority_biscuit(human_keypair: &KeyPair, agent_did: &str) -> Biscuit { 4 let mut builder = Biscuit::builder(); 5 6 // Authority facts 7 builder.add_fact(fact("delegator", &[string("did:adi:human001")])).unwrap(); 8 builder.add_fact(fact("delegate", &[string(agent_did)])).unwrap(); 9 builder.add_fact(fact("right", &[string(agent_did), string("purchase-groceries")])).unwrap(); 10 builder.add_fact(fact("right", &[string(agent_did), string("compare-prices")])).unwrap(); 11 12 // Constraints 13 builder.add_check(check(&[ 14 rule("check1", &[var("time")], 15 &[pred("time", &[var("time")]), 16 Expression::Binary(Op::LessThan, Box::new(var("time")), 17 Box::new(date(2026, 9, 15, 0, 0, 0)))]) 18 ])).unwrap(); 19 20 builder.build(&human_keypair).unwrap() 21 } 22 23 fn attenuate_biscuit(parent: &Biscuit, agent_keypair: &KeyPair, sub_agent_did: &str) -> Biscuit { 24 let mut block = parent.create_block(); 25 26 // Narrow scope: only compare-prices 27 block.add_check(check(&[ 28 rule("check_scope", &[], 29 &[pred("right", &[string(sub_agent_did), string("compare-prices")])]) 30 ])).unwrap(); 31 32 // Add read-only constraint 33 block.add_check(check(&[ 34 rule("check_method", &[var("m")], 35 &[pred("method", &[var("m")]), 36 Expression::Binary(Op::Equal, Box::new(var("m")), Box::new(string("GET")))]) 37 ])).unwrap(); 38 39 parent.append_with_keypair(&agent_keypair, block).unwrap() 40 }
Verifying a Chained Biscuit
1 use biscuit_auth::{Biscuit, Authorizer}; 2 3 fn verify_biscuit(token: &[u8], root_public_key: &PublicKey, action: &Action) -> bool { 4 let biscuit = Biscuit::from_bytes(token, root_public_key).unwrap(); 5 6 let mut authorizer = Authorizer::new(); 7 8 // Provide current context 9 authorizer.add_fact(fact("time", &[date_now()])).unwrap(); 10 authorizer.add_fact(fact("method", &[string(&action.method)])).unwrap(); 11 authorizer.add_fact(fact("resource", &[string(&action.resource)])).unwrap(); 12 authorizer.add_fact(fact("spend", &[integer(action.amount)])).unwrap(); 13 authorizer.add_fact(fact("merchant", &[string(&action.merchant)])).unwrap(); 14 15 // Add authorization policy 16 authorizer.add_policy("allow if right($did, $action)").unwrap(); 17 18 authorizer.authorize_with(&biscuit).is_ok() 19 }
Verification API
Verify an IBCT
1 POST /api/v1/ibct/verify 2 Content-Type: application/json 3 4 { 5 "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IklCQ1Qrand0In0...", 6 "format": "compact-jwt", 7 "expectedAction": { 8 "type": "purchase", 9 "resource": "/api/v1/orders", 10 "method": "POST" 11 }, 12 "expectedAudience": "https://freshmart.com/api" 13 }
Response
1 { 2 "valid": true, 3 "format": "compact-jwt", 4 "agent": { 5 "did": "did:adi:agent:shop01...", 6 "name": "Shopping Assistant", 7 "trustScore": 74 8 }, 9 "delegationChain": { 10 "valid": true, 11 "depth": 1, 12 "root": "did:adi:human001...", 13 "hops": [ 14 { "from": "did:adi:human001...", "to": "did:adi:agent:shop01..." } 15 ] 16 }, 17 "effectiveScope": ["purchase-groceries"], 18 "effectiveConstraints": { 19 "maxSpendPerTransaction": 50, 20 "currency": "USD" 21 }, 22 "actionAuthorized": true 23 }
IBCT Lifecycle
| Property | Value |
|---|---|
| Default TTL | 1 hour |
| Maximum TTL | 24 hours |
| Renewal | Agent requests new IBCT before expiry |
| Revocation | Immediate via delegation VC revocation |
| Audit | Every IBCT verification is logged |
Security Considerations
| Threat | Mitigation |
|---|---|
| Token theft | IBCT is bound to agent’s key pair; replay requires the private key |
| Scope escalation | Biscuit’s append-only structure prevents widening scope |
| Expired delegation | IBCT verification checks delegation VC revocation status in real-time |
| Clock skew | 30-second grace period on nbf and exp claims |
| Replay attack | jti (JWT ID) is unique per token; services track used JTIs |