Capability Tokens (IBCT)

View as Markdown

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:

ProblemIBCT Solution
Bearer tokens can be stolenIBCT is bound to the agent’s DID key pair
No delegation provenanceIBCT chain traces back to human authorization
Static permissionsIBCT encodes exact scope for each invocation
No audit trailEach IBCT is logged with cryptographic proof

Token Formats

IDA supports two IBCT formats depending on the delegation depth:

FormatUse CaseStructureLibrary
Compact JWTSingle-hop delegation (human -> agent)EdDSA-signed JWTjose / jsonwebtoken
Chained BiscuitMulti-hop delegation (human -> agent -> sub-agent)Append-only token with Datalogbiscuit-auth

Compact JWT (Single-Hop)

For simple human-to-agent delegations, IBCTs use a compact EdDSA-signed JWT:

Token Structure

Header.Payload.Signature
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

1import { SignJWT } from 'jose';
2import { importJWK } from 'jose';
3
4async 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

1import { jwtVerify } from 'jose';
2
3async 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
2right("did:adi:agent:shop01", "purchase-groceries");
3right("did:adi:agent:shop01", "compare-prices");
4check if time($time), $time < 2026-09-15T00:00:00Z;
5check if spend($amount), $amount <= 200;
6check if merchant($m), ["FreshMart", "OrganicCo"].contains($m);

Attenuation Block (Agent -> Sub-Agent)

1// Attenuation: Agent delegates narrower scope to Sub-Agent
2check if right("did:adi:agent:price01", "compare-prices");
3// Removed: purchase-groceries (scope narrowed)
4// Added: read-only constraint
5check if method($m), $m == "GET";
6check if time($time), $time < 2026-06-15T00:00:00Z;

Creating a Chained Biscuit

1use biscuit_auth::{Biscuit, KeyPair, builder::*};
2
3fn 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
23fn 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

1use biscuit_auth::{Biscuit, Authorizer};
2
3fn 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

1POST /api/v1/ibct/verify
2Content-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

PropertyValue
Default TTL1 hour
Maximum TTL24 hours
RenewalAgent requests new IBCT before expiry
RevocationImmediate via delegation VC revocation
AuditEvery IBCT verification is logged

Security Considerations

ThreatMitigation
Token theftIBCT is bound to agent’s key pair; replay requires the private key
Scope escalationBiscuit’s append-only structure prevents widening scope
Expired delegationIBCT verification checks delegation VC revocation status in real-time
Clock skew30-second grace period on nbf and exp claims
Replay attackjti (JWT ID) is unique per token; services track used JTIs