Capability tokens

A capability token is a typed, signed authorization. It names an action, optionally carries a versioned JSON scope, and is signed by the granter with their ed25519 key. Covenant enforces agent and tool authorization through capability tokens.

Shape

A token is a SignedCapability — a Capability wrapped with a 64-byte ed25519 signature encoded in base58.

SignedCapability {
  capability: Capability {
    subject:    AgentId,        // who this token authorises
    action:     "tool.web_search",
    scope:      JSON,           // signed scope metadata; {} = unscoped
    granted_by: AgentId,        // who issued the token
    expires_at: u64 | null,     // unix milliseconds; null = never
  },
  signature: [u8; 64],          // ed25519 over canonical_message(capability)
}

Action namespaces

Action strings live in a fixed set of reserved namespaces. The manifest parser and the daemon both validate that actions sit in one of these:

  • intent. — actions over intent dispatch.
  • memory. — actions over memory tiers.
  • identity. — actions over the local identity.
  • tool. — actions over the tool surface, including tool.call.<name> for individual tool dispatch.
  • agent. — actions over agent registration and execution.

Within a namespace, the action string is up to the operator. Examples in active use: tool.web_search, tool.call.echo, memory.write, memory.read.longterm.

Scope contract

Scopes are signed, so mutating the JSON invalidates the token. Grant-time validation rejects malformed non-empty scopes for known action namespaces. Dispatch-time enforcement checks signature validity, expiry, subject, action presence, revocation, the tool.call.* arguments.allow predicate, and the audit.purge before_ms cutoff. It also enforces stable memory predicates for memory.read, memory.read.<tier>, memory.write, memory.purge, memory.repair.*, and memory.compact.*, plus stable A2A predicates for send, receive-admission, respond, and repair flows, plus peer predicates for delegated list/revoke flows and purge retention, plus chain predicates for receipt reads, receipt batch reads, and receipt flushing.

Empty scope {} means unscoped within the named action. Non-empty scopes for known namespaces must be JSON objects with { "version": 1 }.

intent.*    { "version": 1, ... }
agent.*     { "version": 1, ... }
tool.*      { "version": 1, "tool": "echo", "arguments": { "allow": { ... } } }
memory.*    { "version": 1, "tiers": ["working"], "record_id": null, "before_ms": null, "apply": false }
a2a.*       { "version": 1, "peer_pubkey_b58": "...", "task_id": null, "lease_id": null, "duplicate_risk": "idempotent" }
audit.*     { "version": 1, "window": 100, "before_ms": null, "include_integrity": true }
peers.*     { "version": 1, "peer_pubkey_b58": null, "token_prefix": null, "self": null, "force": null, "before_ms": null }
chain.*     { "version": 1, "limit": 100, "mint": null, "cluster": null, "payer_pubkey_b58": null, "resource": null, "batch_id": null }

The repository document docs/capabilities.md tracks the detailed contract. Enforcement hardening should next broaden live coverage for delegated scoped paths.

Canonical encoding

Tokens are signed over a deterministic byte encoding, so any verifier produces the same bytes for the same capability. The encoder is explicit and length-prefixed and is located in a single function (canonical_message); it can be replaced without affecting the wire format.

subject_pubkey       [32 bytes]
action_len_be        [4 bytes, u32 big-endian]
action               [action_len bytes]
scope_len_be         [4 bytes, u32 big-endian]
scope_json_bytes     [scope_len bytes — UTF-8 JSON]
granted_by_pubkey    [32 bytes]
expires_tag          [1 byte: 0 = none, 1 = present]
expires_at_be        [8 bytes, u64 big-endian; zero if expires_tag = 0]

Concatenating these fields in this order produces the message the granter signs and the verifier reconstructs.

Granting a token

Granting is a daemon operation: the daemon constructs a Capability, encodes it canonically, signs it with the local identity key, and persists the signed capability to $COVENANT_HOME/capabilities/granted.jsonl. The CLI and HTTP gateway expose grant as covenant capabilities grant and POST /capabilities/grant.

covenant capabilities grant tool.web_search
covenant capabilities grant memory.write --scope '{"version":1,"tiers":["working"],"apply":true}'
covenant capabilities grant tool.web_search --expires-at 1714938191234

The grant audit event is written alongside the token, and the daemon returns the base58 signature so the operator can revoke the token later.

Verification

Two helpers cover verification:

  • verify(signed) — reconstructs the canonical message and verifies the signature against capability.granted_by.pubkey. Returns BadSignature on mismatch.
  • verify_with_clock(signed, now_ms) — same asverify, plus rejects tokens whose expires_at is in the past relative to now_ms.

At dispatch the daemon enumerates the issuer's active capabilities, discards any that fail verify_with_clock, computes the set of actions, and verifies that every required action from the matched agent's manifest is present. Missing actions are recorded in the audit event and the dispatch is rejected. For tool.call.* grants, an arguments.allow object is enforced as an exact JSON argument match before the tool runs. Scope mismatches emit a separate audit row and return an error.

Revocation

Revocations are tombstones written to $COVENANT_HOME/capabilities/revoked.jsonl. Each tombstone references a token by its base58 signature. The active set is the granted set with revocations subtracted — tokens can be re-granted after revocation, but the prior signature is permanently dead.

covenant capabilities revoke 4qXP…8tF1
# → revoked: 4qXP…8tF1

covenant capabilities revoke 4qXP…8tF1 --json
# → {"kind":"capability_revoked","signature_b58":"4qXP…8tF1","removed":true}

Storage layout

PathFormatAppend-only?
capabilities/granted.jsonlOne SignedCapability per line.Yes.
capabilities/revoked.jsonlOne revocation tombstone per line.Yes.

Security properties

  • Independently verifiable. Anyone with the granter's public key and the canonical encoder can verify a token without contacting the daemon.
  • Tamper-evident. Mutating any field in the capability invalidates the signature.
  • Time-bound. Tokens may carry an expires_at; verifiers check the wall clock.
  • Revocable. The active set is granted ⊝ revoked; the daemon enforces this on every dispatch.
  • Append-only on disk. Both grants and revocations are append-only logs. The audit log records every grant and every revocation; covenant verify flags any capability without a matching grant audit event.

Related

  • Identity and keys — the ed25519 keypair behind every signature.
  • Audit log — where grants, revocations, and capability checks are recorded.
  • Security model — the assumptions the capability layer rests on.