# 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` `before_ms` cutoff, stable memory predicates for `memory.read`, `memory.read.<tier>`, `memory.write`, `memory.purge`, `memory.repair.*`, and `memory.compact.*`, stable A2A predicates for send, receive-admission, respond, and repair flows, peer predicates for delegated list/revoke flows plus purge retention, and chain predicates for receipt reads, receipt batch reads, and receipt flushing 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.<name>`.
- `arguments.allow` is an optional exact JSON argument allowlist. When present on `tool.call.<name>`, 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, and purge.

```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.<tier>` 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.

### `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.

### `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.

## 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` `before_ms` cutoff 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 and chain receipt-read, batch-read, and flush predicates 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`.
