> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.adid.dev/llms.txt.
> For full documentation content, see https://docs.adid.dev/llms-full.txt.

# 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

```json
{
  "alg": "EdDSA",
  "typ": "IBCT+jwt",
  "kid": "did:adi:agent:shop01...#key-1"
}
```

### Payload

```json
{
  "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

```javascript
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

```javascript
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)

```datalog
// 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)

```datalog
// 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

```rust
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

```rust
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

```http
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

```json
{
  "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

| 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          |