Skip to navigation

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
{
"alg": "EdDSA",
"typ": "IBCT+jwt",
"kid": "did:adi:agent:shop01...#key-1"
}

Payload

{
"iss": "did:adi:agent:shop01...",
"sub": "did:adi:agent:shop01...",
"aud": "https://freshmart.com/api",
"iat": 1711612800,
"exp": 1711616400,
"nbf": 1711612800,
"jti": "ibct-f47ac10b-58cc-4372",
"delegator": "did:adi:human001...",
"delegation_vc": "urn:uuid:root-delegation",
"scope": ["purchase-groceries"],
"constraints": {
"maxSpendPerTransaction": 50,
"currency": "USD"
},
"action": {
"type": "purchase",
"resource": "/api/v1/orders",
"method": "POST"
},
"provenance": {
"chain": ["did:adi:human001...", "did:adi:agent:shop01..."],
"rootDelegation": "urn:uuid:root-delegation"
}
}

Creating a Compact IBCT

import { SignJWT } from 'jose';
import { importJWK } from 'jose';
async function createCompactIBCT(agentPrivateKey, delegationVC, action) {
const jwt = await new SignJWT({
delegator: delegationVC.credentialSubject.delegator,
delegation_vc: delegationVC.id,
scope: delegationVC.credentialSubject.scope,
constraints: delegationVC.credentialSubject.constraints,
action: {
type: action.type,
resource: action.resource,
method: action.method,
},
provenance: {
chain: [delegationVC.credentialSubject.delegator, delegationVC.credentialSubject.delegate],
rootDelegation: delegationVC.id,
},
})
.setProtectedHeader({
alg: 'EdDSA',
typ: 'IBCT+jwt',
kid: agentKeyId,
})
.setIssuer(agentDid)
.setSubject(agentDid)
.setAudience(targetService)
.setIssuedAt()
.setExpirationTime('1h')
.setJti(`ibct-${crypto.randomUUID()}`)
.sign(agentPrivateKey);
return jwt;
}

Verifying a Compact IBCT

import { jwtVerify } from 'jose';
async function verifyCompactIBCT(token, resolverService) {
// 1. Decode header to get agent DID and key ID
const { kid } = decodeProtectedHeader(token);
const agentDid = kid.split('#')[0];
// 2. Resolve agent DID Document
const didDoc = await resolverService.resolve(agentDid);
// 3. Extract public key
const verificationMethod = didDoc.verificationMethod.find(vm => vm.id === kid);
const publicKey = importPublicKey(verificationMethod);
// 4. Verify JWT signature
const { payload } = await jwtVerify(token, publicKey, {
issuer: agentDid,
audience: expectedAudience,
});
// 5. Verify delegation chain
const chainValid = await resolverService.verifyDelegationChain(
payload.delegation_vc,
payload.delegator,
agentDid
);
// 6. Verify scope covers the requested action
const scopeValid = checkScopeCoversAction(payload.scope, payload.action);
return { valid: chainValid && scopeValid, payload };
}

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)

// Authority: Human delegates to Agent
right("did:adi:agent:shop01", "purchase-groceries");
right("did:adi:agent:shop01", "compare-prices");
check if time($time), $time < 2026-09-15T00:00:00Z;
check if spend($amount), $amount <= 200;
check if merchant($m), ["FreshMart", "OrganicCo"].contains($m);

Attenuation Block (Agent -> Sub-Agent)

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

Creating a Chained Biscuit

use biscuit_auth::{Biscuit, KeyPair, builder::*};
fn create_authority_biscuit(human_keypair: &KeyPair, agent_did: &str) -> Biscuit {
let mut builder = Biscuit::builder();
// Authority facts
builder.add_fact(fact("delegator", &[string("did:adi:human001")])).unwrap();
builder.add_fact(fact("delegate", &[string(agent_did)])).unwrap();
builder.add_fact(fact("right", &[string(agent_did), string("purchase-groceries")])).unwrap();
builder.add_fact(fact("right", &[string(agent_did), string("compare-prices")])).unwrap();
// Constraints
builder.add_check(check(&[
rule("check1", &[var("time")],
&[pred("time", &[var("time")]),
Expression::Binary(Op::LessThan, Box::new(var("time")),
Box::new(date(2026, 9, 15, 0, 0, 0)))])
])).unwrap();
builder.build(&human_keypair).unwrap()
}
fn attenuate_biscuit(parent: &Biscuit, agent_keypair: &KeyPair, sub_agent_did: &str) -> Biscuit {
let mut block = parent.create_block();
// Narrow scope: only compare-prices
block.add_check(check(&[
rule("check_scope", &[],
&[pred("right", &[string(sub_agent_did), string("compare-prices")])])
])).unwrap();
// Add read-only constraint
block.add_check(check(&[
rule("check_method", &[var("m")],
&[pred("method", &[var("m")]),
Expression::Binary(Op::Equal, Box::new(var("m")), Box::new(string("GET")))])
])).unwrap();
parent.append_with_keypair(&agent_keypair, block).unwrap()
}

Verifying a Chained Biscuit

use biscuit_auth::{Biscuit, Authorizer};
fn verify_biscuit(token: &[u8], root_public_key: &PublicKey, action: &Action) -> bool {
let biscuit = Biscuit::from_bytes(token, root_public_key).unwrap();
let mut authorizer = Authorizer::new();
// Provide current context
authorizer.add_fact(fact("time", &[date_now()])).unwrap();
authorizer.add_fact(fact("method", &[string(&action.method)])).unwrap();
authorizer.add_fact(fact("resource", &[string(&action.resource)])).unwrap();
authorizer.add_fact(fact("spend", &[integer(action.amount)])).unwrap();
authorizer.add_fact(fact("merchant", &[string(&action.merchant)])).unwrap();
// Add authorization policy
authorizer.add_policy("allow if right($did, $action)").unwrap();
authorizer.authorize_with(&biscuit).is_ok()
}

Verification API

Verify an IBCT

POST /api/v1/ibct/verify
Content-Type: application/json
{
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IklCQ1Qrand0In0...",
"format": "compact-jwt",
"expectedAction": {
"type": "purchase",
"resource": "/api/v1/orders",
"method": "POST"
},
"expectedAudience": "https://freshmart.com/api"
}

Response

{
"valid": true,
"format": "compact-jwt",
"agent": {
"did": "did:adi:agent:shop01...",
"name": "Shopping Assistant",
"trustScore": 74
},
"delegationChain": {
"valid": true,
"depth": 1,
"root": "did:adi:human001...",
"hops": [
{ "from": "did:adi:human001...", "to": "did:adi:agent:shop01..." }
]
},
"effectiveScope": ["purchase-groceries"],
"effectiveConstraints": {
"maxSpendPerTransaction": 50,
"currency": "USD"
},
"actionAuthorized": true
}

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