# Capability Scope Contract Capability tokens bind an agent subject, an action string, an optional JSON scope, an issuer, and an optional expiry into one signed object. The signature covers `scope`, so scope fields are tamper-evident. Current enforcement boundary: the daemon validates non-empty scopes for known action namespaces before signing a grant, then enforces action presence, expiry, signature validity, subject matching, revocation, the `tool.call.*` `arguments.allow` predicate, the `audit.purge` and `capabilities.purge` `before_ms` cutoffs, stable memory predicates for `memory.read`, `memory.read.`, `memory.write`, `memory.purge`, `memory.repair.*`, `memory.compact.*`, and `memory.backfill.*`, stable A2A predicates for send, receive-admission, respond, and repair flows, peer predicates for delegated list/revoke flows plus purge retention, chain predicates for receipt reads, receipt batch reads, and receipt flushing, and the `settlement.backfill.*` predicate for receipt backfill at dispatch. ## Scope Envelope Every non-empty scope for a known action namespace must be a JSON object with a version field: ```json { "version": 1 } ``` `{}` remains valid and means unscoped within the named action. Grant requests for known namespaces reject non-object scopes, missing versions, unsupported versions, and malformed known fields. Unknown future fields are preserved as signed metadata until dispatch-time enforcement defines them. ## Namespaces ### `intent.*` and `agent.*` Use the base envelope for intent routing and agent lifecycle actions. Predicate fields are not stable yet, so grant-time validation enforces only the versioned object shape and preserves any extra fields as signed metadata. ### `tool.*` Use for tool listing and tool invocation. ```json { "version": 1, "tool": "echo", "arguments": { "allow": { "text": "optional exact or policy-owned value" } } } ``` Rules: - `tool` is optional for broad actions such as `tool.list`; it should match the suffix for `tool.call.`. - `arguments.allow` is an optional exact JSON argument allowlist. When present on `tool.call.`, the daemon rejects calls whose full argument object does not exactly match it. - Networked tools should add explicit host or origin fields before enforcement. Live HTTP coverage pins this boundary for `tool.call.echo`: a scoped grant rejects a mismatched argument object before dispatch and permits only the exact allowed object. ### `memory.*` Use for memory reads, writes, repair, compaction, purge, and receipt backfill. ```json { "version": 1, "tiers": ["working", "episodic", "longterm"], "record_id": null, "before_ms": null, "apply": false } ``` Rules: - `tiers` is optional; absent means every tier allowed by the action. - `record_id` narrows read, write, and repair operations to one record when the action has a concrete record id. - `before_ms` narrows purge and compaction cutoffs. On read, write, and repair scopes it means the target record must be older than the cutoff. - `apply` distinguishes dry-run/read grants from mutation grants. Reads require `apply: false` when the field is present. `memory.write` and `memory.purge` are always mutations and require `apply: true` when the field is present. - `memory.read` and `memory.read.` gate recent-memory and semantic-search responses. The daemon still filters returned records by authenticated owner, signed tier scope, optional `record_id`, and optional `before_ms`. - `memory.write` is required before successful intent dispatch writes a working-tier memory record. The scope is checked before agent execution or fallback dispatch. - A tier-scoped `memory.purge` grant only permits purging that tier. An un-tiered purge request requires the scope to include all tiers. - A tier-scoped `memory.compact.*` grant only permits policies that touch the listed tiers. `detach_stale_parents` with an explicit tier scope requires all tiers because parent detaches are not tier-isolated. - `memory.backfill.apply` and `memory.backfill.dry_run` are distinct grants for the memory-record receipt-correlation backfill; the backfill mode is part of the action and a scope may pin `apply` to bind a grant to a single mode. `before_ms` bounds the backfill to records at or before a millisecond cutoff (inclusive); `null` or an absent value is unbounded. Grants for `memory.backfill.*` reject `tiers` and `record_id` at validation time because the dispatch predicate does not bind by tier or record. - The `memory backfill-receipt-correlation` command (IPC `BackfillMemoryRecords`, HTTP `POST /memory/records/backfill`) enforces this scope at dispatch: an apply requires `memory.backfill.apply`, a dry run requires `memory.backfill.dry_run`, and the operator identity is required. The backfill correlates every legacy row with no recency filter, so the dispatch probes the scope with an unbounded cutoff — a recency-bounded grant (`before_ms` set) does not authorize a full repair. Correlations are recomputed server-side from the operator's own memory and receipt rows; clients cannot supply correlations directly. ### `a2a.*` Use for agent-to-agent send, receive, respond, repair, and compaction actions. ```json { "version": 1, "peer_pubkey_b58": "base58-public-key", "task_id": null, "lease_id": null, "duplicate_risk": "idempotent" } ``` Rules: - `peer_pubkey_b58` is the canonical peer selector. On `a2a.send.*` it is the recipient. On `a2a.recv.*` and `a2a.respond.*` it is the sender. On `a2a.repair.*` it is the task counterparty visible to the authenticated peer. - `task_id` narrows send, receive-admission, response, and repair flows to one task. - `lease_id` narrows manual repair to one in-flight lease. - `duplicate_risk` narrows `a2a.repair.requeue` posture and should be either `idempotent` or `operator-accepted`; the daemon also accepts the wire spelling `operator_accepted`. ### `audit.*` Use for audit reads, verification, and retention. ```json { "version": 1, "window": 100, "before_ms": null, "include_integrity": true } ``` Rules: - `window` bounds read and verification work. - `before_ms` narrows purge authority. When present on `audit.purge`, the requested purge cutoff must be less than or equal to the scoped value. - `include_integrity` records whether the grant covers hash-chain verification, not only event reads. ### `capabilities.*` Use for revoked-capability retention. `capabilities.purge` gates `covenant capabilities purge` (IPC `Request::PurgeCapabilities`, HTTP `POST /capabilities/purge`), the garbage collection that removes revoked-capability rows by a millisecond cutoff. ```json { "version": 1, "before_ms": null } ``` Rules: - `before_ms` narrows purge authority: when present on `capabilities.purge`, the requested cutoff must be less than or equal to the scoped value; `null` or an absent field is unbounded. The dispatch predicate is the same `before_ms` cutoff enforced for `audit.purge` and the `peers.purge` retention sweep. - The operator identity remains the root authority for capability-registry control. A scoped `capabilities.purge` grant is delegated retention authority for a non-operator peer. - Grant-time validation does not yet bind the `capabilities.*` namespace, so a non-empty scope is preserved as signed metadata at grant time and only `before_ms` is interpreted at dispatch. Treat the cutoff as an enforced dispatch bound, not a grant-time-validated envelope. ### `peers.*` and `identity.*` Use for peer registry and local identity operations. ```json { "version": 1, "peer_pubkey_b58": null, "token_prefix": null, "self": null, "force": null, "before_ms": null } ``` Rules: - The operator identity remains the root authority for local peer-registry control. Scoped peer grants are delegated authority for non-operator peers. - `peer_pubkey_b58` narrows delegated `peers.list`; when present, it must decode to a 32-byte base58 public key and the request must use the exact target pubkey as `pubkey_prefix`. - `token_prefix` narrows delegated `peers.revoke`; when present, it must be a non-empty base58 prefix. The requested token prefix must start with the scoped prefix, and the daemon's normal ambiguity checks still run before mutation. - `force` narrows delegated revoke requests when present. `force: false` permits only non-force revocations; `force: true` permits only force revocations. - `before_ms` narrows `peers.purge`; the requested cutoff must be less than or equal to the scoped value. - `self` is reserved for self-targeting peer and identity operations; when present, it must match the daemon's concrete self-target predicate. Live coverage pins this boundary through a non-operator `peers.revoke` case: missing grants are denied, mismatched `token_prefix` scopes are denied before mutation, and a matching token-prefix scope revokes only the scoped target. ### `chain.*` Use for local settlement and receipt batching. ```json { "version": 1, "limit": 100, "mint": null, "cluster": null, "payer_pubkey_b58": null, "resource": null, "batch_id": null } ``` Rules: - `chain.receipts` gates local receipt reads; `chain.batches` gates local receipt batch summaries; `chain.flush` gates local receipt batching. - `limit` bounds read and batch sizes. A request above the scoped limit is rejected before receipts are read or batched. - `payer_pubkey_b58` narrows receipt rows to a 32-byte base58 payer key. The daemon still applies the authenticated-payer filter first. - `resource` narrows receipt rows to `compute`, `memory`, `tool`, `message`, or `registration`. - `cluster` and `batch_id` narrow already-batched receipt rows. Unbatched local receipts do not satisfy a concrete `cluster` or `batch_id` selector. - `mint` is checked against the configured settlement mint for `chain.flush`; a concrete mint selector does not match if the daemon has no configured mint. ### `settlement.*` Use for local settlement-receipt maintenance. ```json { "version": 1, "apply": true, "before_ms": null } ``` Rules: - `settlement.backfill.apply` and `settlement.backfill.dry_run` are distinct grants; the backfill mode is part of the action. A scope may pin `apply` to bind a grant to a single mode. - `before_ms` bounds the backfill to receipts at or before a millisecond cutoff (inclusive); `null` or an absent value is unbounded. - The `settlement backfill-receipts` command (IPC `BackfillSettlementReceipts`, HTTP `POST /settlement/receipts/backfill`) now enforces this scope at dispatch: an apply requires `settlement.backfill.apply`, a dry run requires `settlement.backfill.dry_run`, and the operator identity is required. The backfill repairs every legacy row with no recency filter, so the dispatch probes the scope with an unbounded cutoff — a recency-bounded grant (`before_ms` set) does not authorize a full repair. ## Enforcement Path 1. Keep accepting `{}` for existing broad grants. 2. Validate non-empty scopes at grant time for known action namespaces. 3. Interpret the stable `tool.call.*` `arguments.allow` predicate at dispatch. 4. Interpret the stable `audit.purge` and `capabilities.purge` `before_ms` cutoffs at dispatch. 5. Interpret stable memory read, write, purge, repair, and compaction predicates at dispatch. 6. Interpret stable A2A peer, task, lease, and duplicate-risk predicates at dispatch. 7. Interpret stable peer-registry list/revoke/purge predicates, chain receipt-read, batch-read, and flush predicates, and the settlement receipt-backfill predicate at dispatch. 8. Fail closed for malformed versioned scopes after a migration window. 9. Keep action-only checks as the fallback only for unscoped operator grants. Until a namespace-specific predicate lands, public docs must describe that namespace's scope as validated signed metadata and compatibility preparation, not as enforced least-privilege behavior. ## Machine-readable inspection `covenant capabilities recent --json` emits one stable object for supervisors: ```json { "kind": "capability_list", "limit": 10, "capabilities": [] } ``` Each row is the wire `SignedCapability`, including `capability`, `scope`, optional `expires_at`, and the base58-encoded `signature`. Human output from `covenant capabilities recent` remains unchanged. Retention maintenance has the same machine-readable convention: ```json { "kind": "capabilities_purged", "before_ms": 1700000000000, "purged": 0 } ``` `before_ms` is the effective cutoff, including values derived from `--older-than-ms`.