# ActionPass v0 — Payload-Bound Authorization for Agent Actions

Status: Draft (v0, tracks `axtary.actionpass.v0` as implemented in `@axtary/actionpass@0.1.x`)

Date: 2026-06-11

Editors: Axtary

## Abstract

An **ActionPass** is a signed, short-lived, revocable authorization artifact for a single AI-agent action. Unlike a bearer credential, which authorizes a *channel* (an API, a scope, a tool), an ActionPass authorizes *content*: it binds the authorization decision — and, when required, an exact human approval — to the SHA-256 hash of the action's canonical payload. An agent holding a valid pass cannot change what the pass authorizes; any mutation of the payload after issuance or approval is detectable by any verifier, offline, before execution.

This document specifies the ActionPass token format as a strict profile of JWS/JWT, the canonical action model and hashing rules, the approval-artifact binding, the fail-closed verification algorithm, revocation, and the hash-chained decision ledger that makes every authorization and outcome tamper-evident. A conforming verifier needs only this document, an off-the-shelf JOSE library, and the issuer's public keys: see [Test Vectors](#12-test-vectors).

## 1. Relationship to Existing Standards

ActionPass is deliberately a *profile and composition* of existing mechanisms rather than a new cryptographic design:

| Mechanism | Standard | How ActionPass uses it |
|---|---|---|
| Token format | JWS Compact (RFC 7515), JWT claims (RFC 7519) | The pass is a JWT with registered claims plus an `axtary.actionpass.v0` private-claim set (§5). |
| Algorithm discipline | JWT BCP (RFC 8725) | Fixed algorithm allowlist, `alg` from the header is never trusted to select verification behavior, keys resolved by `kid` against a local trust store, fail closed on unknown `kid` (§5.1, §8). |
| Delegation semantics | OAuth 2.0 Token Exchange (RFC 8693) | Issuance is an exchange: the agent presents an intended action (the "subject" context); the issuer evaluates policy and returns a narrower, single-action credential. `sub` is the agent, `aud` is the exact target resource (§5.2). |
| Proof-of-binding | DPoP (RFC 9449), Proof-of-Possession semantics (RFC 7800) | Where DPoP binds a token to a *key* held by the client, ActionPass binds the token to the *content* of one action via `payloadHash`. The two are complementary: DPoP constrains who can present a token; ActionPass constrains what the token authorizes (§5.3, §13.1). |
| Canonical JSON | JSON Canonicalization Scheme / JCS (RFC 8785) | All hashed JSON is serialized with sorted keys in UTF-16 code-unit order and ECMAScript `JSON.stringify` literal forms (§3). |
| Audit | Transparency-log chaining (in the spirit of RFC 9162) | Every decision and execution outcome is a ledger record carrying the hash of its predecessor; truncation, reordering, and in-place edits are detectable from the records alone (§9). |

## 2. Terminology

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.

- **Action** — a single intended tool call by an agent, normalized to the schema in §4.
- **Issuer** — the component that evaluates policy and signs passes. In Axtary this is the local proxy; the agent never holds signing keys or provider credentials.
- **Verifier** — any component that checks a pass against an action before execution (the executing adapter, a second proxy, an auditor).
- **Approval artifact** — a record of an exact human (or explicit policy-override) approval, bound to the action and payload hashes (§6).
- **Ledger** — the append-only, hash-chained record of decisions and execution outcomes (§9).

## 3. Canonicalization and Hashing

All hashes in this specification are computed as:

```
hash = "sha256:" || lowercase_hex( SHA-256( UTF-8( canonical_json(value) ) ) )
```

and match the regular expression `^sha256:[a-f0-9]{64}$`.

`canonical_json` is JCS-aligned (RFC 8785) for the JSON value model used here:

1. Object members with the value `undefined` are removed.
2. Object member names are sorted by UTF-16 code units, ascending. Sorting MUST NOT be locale-sensitive.
3. Arrays preserve their order.
4. The structure is serialized with ECMAScript `JSON.stringify` (no whitespace; RFC 8785 number and string literal forms).

Values to be hashed MUST be JSON-safe: strings, finite numbers, booleans, `null`, arrays, and plain objects. Non-finite numbers and non-JSON types are rejected.

Three hashes recur throughout:

- **`payloadHash`** = hash of `action.capability.payload` only. This is what approvals and passes bind to.
- **`actionHash`** = hash of the entire normalized action (§4) after schema validation (including defaulted fields such as `schemaVersion`).
- **`artifactHash`** = hash of a complete approval artifact (§6).

## 4. Normalized Action (`axtary.action.v0`)

Every gated tool call is first normalized to this shape. The schema is closed for the fields below; producers MUST populate them and verifiers MUST re-derive hashes from this normalized form, never from transport-level requests.

```json
{
  "schemaVersion": "axtary.action.v0",
  "actor": {
    "agentId": "agent:codex-prod",
    "humanOwner": "user:asrar@company.com",
    "runtime": "codex-cli",
    "tenant": "org:company"
  },
  "intent": {
    "taskId": "AXT-418",
    "declaredGoal": "Open a PR that fixes the auth redirect bug",
    "maxDelegationDepth": 1
  },
  "capability": {
    "tool": "github.pull_requests.create",
    "resource": "repo:company/web-app",
    "payload": { "...": "tool-specific JSON" },
    "constraints": { "...": "optional tool-specific limits" }
  },
  "toolDefinition": {
    "serverIdentity": "mcp://internal/payments",
    "schemaVersion": "2026-03-26",
    "definitionHash": "sha256:...",
    "publisher": "mcp-publisher://acme",
    "publisherKeyId": "...",
    "semver": "1.2.0",
    "priorDefinitionHash": "sha256:..."
  },
  "budget": {
    "reservationId": "budget_<uuid>",
    "cost": { "actions": 1 },
    "limit": { "actions": 100 },
    "commitStatus": "pending"
  }
}
```

- `actor` — the agent identity, the accountable human owner, and the agent runtime. `tenant` is optional at the action level (the pass carries the authoritative tenant).
- `intent.taskId` / `declaredGoal` — the work item and stated purpose; bound into the pass and checkable at verification.
- `capability.tool` — namespaced tool name (e.g. `github.contents.write`, `mcp.tool.call`). `resource` — the exact target (repository, channel, server/tool URI). `payload` — the complete content of the action; this is the hashed surface.
- For `postgres.query.select`, the resource is
  `postgres:database/<database>` and the payload contains the exact SQL
  statement, positional parameter values, and maximum rows. The pass binds all
  three. Provider evidence MUST omit parameter values and returned rows; it
  records statement/predicate/parameter hashes, parsed tables/functions, row
  count, returned field names, database/role identity, read-only state, and RLS
  evidence.
- For `drive.files.read`, the resource is `drive:file:<fileId>` and the
  payload contains the same exact `fileId` plus a positive `maxBytes`. Policy
  MUST allowlist the file id and bind it to the resource before execution. The
  adapter MUST fetch provider metadata first and require
  `isAppAuthorized=true`; an arbitrary or unpicked file fails closed before
  content download. Authorization evidence records only file id, requested
  `drive.file` scope, and byte cap. Execution evidence MAY add provider-returned
  title, MIME type, app-authorization state, byte count, and URL, but MUST omit
  OAuth credentials and file content.
- `toolDefinition` (optional, REQUIRED for `mcp.tool.call`) — provenance pin for the executing tool: server identity, schema version, and the hash of the full tool definition. A definition that changes after approval changes this hash and MUST cause re-authorization (the tool-poisoning counter). The optional `publisher`, `publisherKeyId`, `semver`, and `priorDefinitionHash` fields carry verified signed-publisher provenance for MCP signed definitions (`axtary.mcp_signed_definition.v1`, see `spec/mcp-tool-provenance-v1.md`); they are absent for hash-only (unsigned) tools, so an unsigned action's `toolDefinition` and its hash are unchanged from prior profiles.
- `provenance` (optional) — authority-owned `axtary.provenance.v0`
  field-to-source integrity bindings. Caller-supplied final labels are rejected
  by the proxy. See `spec/provenance-v0.md`.
- `budget` (optional, authority-owned) — the proxy adds a reservation id,
  configured metered cost/limit, and initial `pending` state after policy allow
  and before pass issuance. Agent-supplied reservation fields MUST be rejected.

## 5. The ActionPass Token

An ActionPass is a JWS Compact Serialization JWT.

### 5.1 Protected Header

```json
{ "alg": "ES256", "typ": "axtary-actionpass+jwt", "kid": "axtary-key-2026-05" }
```

- `alg` — MUST be on the verifier's fixed allowlist. v0 issuers sign with **ES256**. Verifiers MUST pin the allowlist out-of-band and MUST reject any token whose `alg` is not on it; in particular `none` and symmetric MACs are rejected. The header's `alg` never *selects* trust — the verification key and its permitted algorithm come from the trust store entry resolved by `kid` (§8 step 2).
- `typ` — SHOULD be `axtary-actionpass+jwt`; verifiers SHOULD reject other types to prevent cross-protocol token confusion (RFC 8725 §3.11).
- `kid` — identifies the verification key in the issuer's published key set / verifier's trust store. When the verifier holds more than one key, `kid` is REQUIRED and an unknown `kid` MUST fail verification (no fallback key).

### 5.2 Claims

Registered claims (RFC 7519):

| Claim | Value |
|---|---|
| `iss` | Issuer URL/identifier of the signing proxy or control plane. |
| `sub` | `action.actor.agentId` — the agent the pass is issued to. |
| `aud` | `action.capability.resource` — the exact target resource. A pass for one repository is not valid for another. |
| `iat`, `nbf` | Issuance time (seconds). `nbf` equals `iat` in v0. |
| `exp` | `iat` + TTL. The default TTL is **600 seconds**; policy MAY narrow it per decision. Passes are short-lived by design. |
| `jti` | Unique pass id (`ap_<uuid>`); the handle for revocation (§7) and ledger correlation. |

Private claims (`axtary.actionpass.v0`):

| Claim | Value |
|---|---|
| `apv` | Literal `"axtary.actionpass.v0"` — claim-set version. |
| `tenant` | Issuing tenant. |
| `humanOwner` | `action.actor.humanOwner` — accountable human. |
| `agentRuntime` | `action.actor.runtime`. |
| `intent` | Copy of `action.intent` (taskId, declaredGoal, optional maxDelegationDepth). |
| `capability` | `{ tool, resource, constraints? }` — the payload itself is NOT embedded; only its hash is. |
| `decision` | Always `"allow"` — a pass is only ever issued for an allow (possibly an allow reached *via* approved step-up). Deny and pending step-up decisions never produce passes. |
| `reasons` | Policy rule identifiers that produced the allow. |
| `policy` | `{ nativeRule, version, cedarCompatible, opaCompatible }` — which policy, at which version, decided. |
| `payloadHash` | §3 hash of `capability.payload`. **The content binding.** |
| `provenanceHash` | Present when provenance is attached: SHA-256 over canonical `axtary.provenance.v0`. |
| `budget` | Optional authority-owned `{ reservationId, cost, limit, commitStatus: "pending" }`. Verifiers bind it to the presented action; settlement is recorded in the ledger (§9.2). |
| `approval` | Present iff the allow required approval: `{ mode, approvedBy, approvedAt, approvalArtifact (id), approvalArtifactHash, actionHash, payloadHash }` (§6). |
| `audit` | `{ traceId, parentPassId, ledgerHash }` — distributed-trace id, optional parent pass for delegation chains, and the hash of the ledger record current at issuance, tying the pass into the transparency chain (§9). |

### 5.3 What the pass does and does not embed

The pass embeds *hashes* of the payload and approval artifact, not their contents. Consequences:

- The token stays small and free of secrets/PII from payloads; it can be logged and exported safely.
- A verifier MUST be presented with the action it is asked to execute, recompute `payloadHash`, and compare (§8). The pass alone proves nothing about an action that is not presented with it.
- Possession of the token is not possession of authority over *different* content — the anti-exfiltration property. Sender-constraining is normative in the backward-compatible v1 profile (`spec/actionpass-v1.md`).

### 5.4 Policy rule and obligation binding

The issuer MAY evaluate the normalized action against an ordered set of named
rules. A rule matches tool, resource, and actor selectors and yields
`allow`, `deny`, or `step_up`; unmatched actions MUST deny. When multiple rules
match, `deny` outranks `step_up`, which outranks `allow`.

Policy obligations are enforcement inputs, not advisory metadata:

- a narrowed TTL sets the decision's `constraints.expiresInSeconds`, which the
  issuer uses for `exp`;
- blocked path prefixes and UTC time/rate windows are checked before issuance;
- required approver roles turn an otherwise-allowing rule into `step_up`, and
  the approval artifact MUST carry one of the required attributed roles.

The decision may carry the determining rule id, all matched rule ids, and the
merged obligations. These fields are ledger evidence; the pass continues to
bind the durable policy identity/version plus the resulting reasons and TTL.

After the base engine, the issuer MAY apply opt-in **post-engine guards** that
append reasons and may only **escalate** a decision (raise `allow` to `step_up`,
or — for provenance — to `deny`); they never weaken a `deny`/`step_up` and never
produce an `allow`. These reasons appear in the decision and ledger records
(§9) alongside rule reasons:

- `provenance_unknown_egress` / `provenance_untrusted_egress` — deterministic
  field-lineage egress guard (`spec/provenance-v0.md`).
- `behavior_first_seen` / `behavior_abnormal_sequence` — deterministic
  novelty/sequence step-up, escalate-only (`spec/behavior-step-up-v0.md`).

## 6. Approval Artifacts (`axtary.approval.v0`)

A step-up decision is satisfiable only by an approval bound to the exact content the human saw:

```json
{
  "schemaVersion": "axtary.approval.v0",
  "id": "approval_<uuid>",
  "mode": "human",
  "approvedBy": "user:reviewer@example.com",
  "approverRoles": ["security-reviewer"],
  "approvedAt": "2026-06-11T00:00:00.000Z",
  "actionHash": "sha256:...",
  "payloadHash": "sha256:...",
  "provenanceHash": "sha256:... | omitted",
  "taskId": "AXT-418",
  "tool": "github.pull_requests.create",
  "resource": "repo:company/web-app",
  "reason": "Reviewed the exact PR payload",
  "expiresAt": "2026-06-11T00:10:00.000Z"
}
```

`mode` is `human` or `policy_override` (an explicit, attributed override — never silent). `approverRoles`, when present, is the set of roles attributed by the approval authority and is checked against policy obligations. At issuance the issuer MUST validate the artifact against the action being authorized; every check failing is fatal (no pass):

1. `payloadHash` equals the recomputed payload hash — else `approval_payload_hash_mismatch`.
2. `actionHash` equals the recomputed action hash — else `approval_action_hash_mismatch`.
3. `taskId`, `tool`, `resource` equal the action's — else the corresponding `approval_*_mismatch`.
4. `expiresAt`, if present, has not passed — else `approval_artifact_expired`.

The payload check runs first so a swapped payload — which necessarily changes both hashes — reports the precise `approval_payload_hash_mismatch` rather than the generic action mismatch; this matches the verification-time claim order in §8 step 6. Both checks always run and every failure is fatal.

The issued pass's `approval` claim then records the artifact id, the artifact's own hash (`approvalArtifactHash` = §3 hash of the full artifact), and both binding hashes. An auditor holding the artifact can re-derive `approvalArtifactHash` and confirm the pass was issued against exactly that artifact.

This is the property demonstrated by the tamper test: after approval, a mutated payload re-hashes differently, binding check (1) fails, and execution is blocked before the provider is called — even though the holder still possesses a "valid" signed token.

## 7. Revocation

A revocation is a small durable trust-store record:

```json
{ "schemaVersion": "axtary.actionpass_revocation.v0", "passId": "ap_...", "revokedAt": "...", "revokedBy": "user:...", "reason": "..." }
```

Revocation is by `jti`. Verifiers MUST check the pass `jti` against their
revocation source on every presentation and reject revoked passes with
`actionpass_revoked`. If the configured source cannot be read or queried,
verification MUST fail closed with `actionpass_revocation_check_failed`; source
failure is never equivalent to an empty revocation set. Because TTLs are short
(§5.2), the revocation window any verifier must track is bounded by `exp`.

The reference local runtime stores these records in
`.axtary/actionpass-trust-store.json` (overrideable by the operator), writes the
file atomically with mode `0600`, and exposes `axtary revoke <jti>` plus
`axtary revocations`. The record is not independently signed: its authority is
the trusted local filesystem boundary. v0 distribution is therefore local and
pull-based. Authenticated push/offline distribution across hosts is a separate
issuer-deployment protocol, not a property claimed by this token format.

The trust-store record above is the **enforcement authority** — the only source
verifiers check. In addition, a revoke SHOULD record a **tamper-evident
revocation event** in the hash-chained ledger (§9): a `deny` record naming the
pass (`actionPassId`) with the `actionpass_revoked` reason, bound to the
revoked action's `actionHash`/`payloadHash`. This is an auditable logbook entry,
not the enforcement source; it MUST be written after the durable trust-store
revocation so an audit-append failure can never leave a pass enforced-but-not
-revoked. Where no ledger record names the pass, the trust-store revocation
stands alone and no event is fabricated. Both the `axtary revoke` CLI and the
permission-guarded dashboard revoke (local mode, `ledger:revoke`) follow this
single path; a hosted dashboard holds synced copies only and MUST NOT present a
revoke control that cannot reach the runtime's authority.

## 8. Verification Algorithm

Given: a token, the normalized action to be executed, the expected issuer, and a trust store (keys + revocations). All failures are terminal; there are no advisory results. A verifier MUST execute, in order:

1. **Parse the action** against the `axtary.action.v0` schema. Malformed → reject.
2. **Resolve the key**: read `kid` from the protected header; look up an `active` key in the trust store. Missing or unknown `kid` (when a keyring is in use) → reject (`verification_key_not_found:<kid>`). The key record, not the token, determines the permitted algorithm.
3. **Verify the JWS** with the resolved key, requiring: signature validity; `alg` on the fixed allowlist; `iss` equals the expected issuer; `aud` contains `action.capability.resource`; current time within `nbf`/`exp` (a small configured clock tolerance MAY apply).
4. **Validate the claim set** against the `axtary.actionpass.v0` schema (including `apv`). Unknown versions → reject.
5. **Check identity binding** (each failing check rejects with the listed reason):
   - `sub` = `action.actor.agentId` (`agent_id_mismatch`)
   - `humanOwner` = `action.actor.humanOwner` (`human_owner_mismatch`)
   - `agentRuntime` = `action.actor.runtime` (`agent_runtime_mismatch`)
   - `intent.taskId` = `action.intent.taskId` (`task_id_mismatch`)
   - `capability.tool` = `action.capability.tool` (`tool_mismatch`)
   - `capability.resource` = `action.capability.resource` (`resource_mismatch`)
   - `intent`, capability constraints, and optional authority-owned `budget`
     MUST match canonically (`intent_mismatch`, `constraints_mismatch`,
     `budget_mismatch`).
6. **Check content binding**: recompute `payloadHash` (§3) from the presented action; it MUST equal the claim (`payload_hash_mismatch`). If an `approval` claim is present, its `payloadHash` and `actionHash` MUST also equal the recomputed values (`approval_payload_hash_mismatch`, `approval_action_hash_mismatch`). When provenance is present, recompute `provenanceHash`; mutation, removal, or an unexpected claim MUST reject (`provenance_hash_mismatch` / `unexpected_provenance_hash`).
7. **Check revocation**: query the configured source on this presentation. Its
   failure MUST reject (`actionpass_revocation_check_failed`); `jti` appearing
   in the source MUST reject (`actionpass_revoked`).

Only a token passing all seven steps authorizes execution — and only of the exact presented action.

## 9. The Decision Ledger (`axtary.ledger.v0`)

Every decision (allow, deny, step_up) and every execution outcome is appended as a ledger record:

```json
{
  "schemaVersion": "axtary.ledger.v0",
  "id": "ledger_<uuid>",
  "occurredAt": "2026-06-11T00:00:00.000Z",
  "auditContext": {
    "tenant": "org:company",
    "agentId": "agent:codex-prod",
    "humanOwner": "user:asrar@company.com",
    "taskId": "AXT-418",
    "tool": "github.pull_requests.create",
    "resource": "repo:company/web-app"
  },
  "actionHash": "sha256:...",
  "payloadHash": "sha256:...",
  "decision": "allow",
  "reasons": ["github_pr_inside_policy"],
  "policy": { "nativeRule": "...", "version": "...", "cedarCompatible": true, "opaCompatible": true },
  "providerEvidence": { "...": "normalized, sanitized provider summary" },
  "executionOutcome": { "...": "present on outcome records; status, result id/URL, failure class, optional approvedPayloadHash + executedPayloadHash equivalence pair" },
  "traceId": "trace_...",
  "actionPassId": "ap_... | null",
  "budget": { "...": "optional denied/pending/committed/rolled_back meter event" },
  "correlationId": "links an outcome record to its decision record",
  "previousLedgerHash": "sha256:... | null",
  "ledgerHash": "sha256:..."
}
```

Chaining rule: `ledgerHash` = §3 hash of the record *without* its `ledgerHash` member; `previousLedgerHash` MUST equal the previous record's `ledgerHash` (`null` only for the first record). A verifier re-walks the file recomputing both; any edit, deletion, reordering, or truncation breaks the chain. Decision and outcome records for one action share `correlationId`, so "what was authorized" and "what actually happened" are auditable as a pair. The pass's `audit.ledgerHash` (§5.2) pins the chain state at issuance.

**Approval↔execution equivalence.** When an executed action carried approval
evidence, the runtime MUST stamp two optional fields on
`axtary.ledger_execution_outcome.v0`: `approvedPayloadHash` — the payload hash
the approval was bound to (§6) — and `executedPayloadHash` — the §3 hash of
the payload actually executed, recomputed at write time. The pair makes
`approved_payload_hash == executed_payload_hash` a first-class, queryable,
exportable proof rather than an inference. Export verification (attestation
verify, §9.1) MUST reject an export whose execution record carries an
incomplete pair (`equivalence_pair_incomplete`), an `executedPayloadHash` that
disagrees with the record's own hash-chained `payloadHash`
(`equivalence_executed_hash_mismatch` — a forged outcome block), or
`approvedPayloadHash != executedPayloadHash`
(`approval_execution_equivalence_failed` — an execution that escaped
enforcement). An execution record with no pair is *unproven* (no approval
evidence, or written before this field existed) and MUST NOT fail verification
or be presented as proved. Both fields are optional and additive: existing
records, exports, and verifiers are unaffected.

`auditContext` is a payload-free search projection copied from the normalized
action: tenant (or `null` when absent), agent, accountable human owner, task,
tool, and resource. It MUST be derived by the trusted ledger writer, never
accepted as a caller-supplied replacement for the normalized action. It is
covered by `ledgerHash`. The member is optional when reading historical v0
records written before this projection existed; an operator surface MUST label
those dimensions unavailable rather than infer them from hashes or unrelated
provider evidence.

OpenTelemetry export is a non-normative projection of ledger records, not a
replacement for the JSONL ledger or detached attestation. The reference
implementation MAY export opt-in OTLP/HTTP trace spans with
`gen_ai.operation.name = "execute_tool"`, `gen_ai.tool.name`, and
`gen_ai.tool.type = "function"` derived from `auditContext.tool` (or sanitized
provider evidence when historical records lack `auditContext`). A conforming
projection MUST NOT include normalized payload bodies, tool arguments, provider
tokens, headers, result rows, file contents, or raw shell commands. It MAY carry
hash-bound and payload-free fields already present in the ledger, including
decision, reasons, action hash, payload hash, ledger hash, trace id, policy
version/rule, pass id, and audit dimensions. OTLP delivery failure does not
alter the ledger record or its hash; implementations that explicitly enable
live export should report the failed export truthfully rather than implying
observability evidence was delivered.

For a native connector, the reference implementation MUST resolve
`providerEvidence` by exact tool name through a registered descriptor
normalizer, not by guessing from provider prefixes or accepting caller-supplied
evidence. The descriptor also carries capability/config/doctor metadata, but
those fields do not grant authority: the normalized action, policy decision,
ActionPass, and adapter verification remain normative. Unknown tools yield no
provider evidence and remain subject to default-deny policy. This requirement
does not change the `axtary.ledger_provider_evidence.v0` schema.

For GitHub review comments, check-runs, and issue comments, provider evidence
MUST bind the exact target and outbound content needed to reconstruct the
authorized write: pull/issue number, commit/path/line/side, or check name/head
SHA/status/conclusion/output as applicable. A successful adapter outcome MAY
copy GitHub's non-secret `X-Accepted-GitHub-Permissions` and rate-limit response
metadata into `executionOutcome.sideEffectProof`; it MUST NOT copy the
authorization header or token. Provider rate-limit exhaustion is a failed
execution outcome, never a successful side-effect claim. This uses the existing
open `sideEffectProof` map and does not change the ledger profile.

A read-only operator surface MAY reconstruct **delegation lineage** and
**budget state** from these records, but only from recorded values. Delegation
lineage is derived solely from the recorded edge `{ parentPassId, childPassId,
depth, rootPassId }` (§9); a conformant surface presents the recorded depth
progression and parent→child structure and MUST NOT display attenuated scope
sets, because the ledger records pass identifiers and depth, not scopes (scope
attenuation is enforced at issuance and verification, §8). Budget state is
derived from the recorded budget events (§9.2): committed and reserved usage and
the configured limit are read from a scope's latest event, and over-budget
denials are counted from `denied` events. A conformant surface MUST NOT infer a
limit, reservation, or authority that is absent from the records — a unit with
no recorded limit is reported as unbounded, never as a fabricated cap.

A read-only operator surface MAY also present **aggregate decision activity** —
counts of `allow`/`deny`/`step_up`, activity bucketed over time, and the most
frequent enforcement reasons — but each value MUST be a direct recount of
recorded ledger records (and, for approval state, recorded approval artifacts).
Time-series and trend visualizations MUST bucket real record timestamps; they
MUST NOT synthesize, smooth, or extrapolate a series. A tenant with no recorded
decisions MUST render an explicit empty/cold-start state, never invented
activity, and such a surface MUST NOT present a derived monetary or
cost-savings metric, because the ledger records authorization decisions and
execution outcomes, not spend.

When provenance is present, `provenanceHash` is repeated explicitly in the
pass and ledger. It is also transitively covered by `actionHash`, because the
provenance structure is part of the normalized action.

The ledger is local-first JSONL. A conforming local writer MUST serialize
appenders with an inter-process lock, resolve the current chain head while
holding that lock, append exactly one newline-terminated record without
rewriting prior bytes, and synchronize the ledger file before reporting
success. If creation or an atomic head-checkpoint rename changes a directory
entry, the containing directory MUST also be synchronized on platforms that
support directory `fsync`.

The reference writer keeps a private
`axtary.local-ledger-head.v0` checkpoint beside the JSONL file containing only
its byte length, record count, and last ledger hash. This checkpoint is an
O(1) acceleration hint, not audit evidence: a missing or stale checkpoint is
recovered by re-verifying the JSONL chain under the lock. A writer killed after
the ledger-file sync but before the checkpoint update therefore leaves the
record durable; the next writer recovers it before appending. Hosted sync
uploads *verified* exports only.

### 9.2 Budget reservation lifecycle

Budget metering is local runtime enforcement, not billing. Limits and per-tool
costs come from trusted proxy configuration; the agent cannot choose its cost,
limit, reservation id, or settlement state. An enabled meter MUST have a
wildcard base cost and a limit for every configured cost dimension; incomplete
authority configuration fails at startup.

For a budget-enabled allow:

1. The meter computes the configured cost and atomically checks
   `committed + pending + proposed <= limit` for every dimension. Pending
   reservations count, preventing concurrent oversubscription.
2. If any configured dimension would exceed its limit, no pass is issued and a
   deny record is written with reason `budget_exceeded` and budget event
   `status: "denied"`.
3. Otherwise the meter creates a reservation. The action and pass carry its
   `pending` budget binding; the decision record carries a hash-bound
   `status: "pending"` event.
4. Successful execution commits the reservation and writes
   `status: "committed"` on the correlated outcome record. Failed execution
   rolls it back and writes `status: "rolled_back"`.

Each budget event contains `reservationId` (null only for a denied attempt),
scope, expiry, configured cost/limit, committed usage before/after, and
pending reserved usage after the transition. The shipped local meter reconstructs committed and
unexpired pending state from the verified ledger after restart. Pending
reservations expire with their ActionPass TTL during reconstruction.

`authorize`-only transports do not expose an execution callback. They
conservatively commit when the pass is issued, so they cannot produce an
unmetered executable pass or an orphaned pending reservation.

This profile defines numeric usage dimensions (for example `actions`,
`externalMessages`, or `apiCalls`). It does not define prices, invoices,
customer billing, or payment processing.

### 9.1 Ledger Attestation (`axtary.ledger_attestation.v0`)

The in-process chain (§9) proves integrity to whoever holds the file. An **attestation** extends that to an *offline third party*: a detached signature over a ledger **export** that an auditor verifies with nothing but a JOSE library and §3 of this document.

A ledger export is the JSON object `{ schemaVersion, exportedAt, source: { filePath, totalRecords, lastLedgerHash }, filters, records[] }`. An attestation is a JWS Compact JWT, signed under the same algorithm discipline as an ActionPass (§5.1; ES256, fixed allowlist, `kid`-resolved key):

- Protected header: `{ "alg": "ES256", "typ": "axtary-ledger-attestation+jwt", "kid": "..." }`. Verifiers MUST reject other `typ` values (cross-protocol confusion) and any `alg` off the allowlist.
- Claims (`lav` = literal `axtary.ledger_attestation.v0`):

| Claim | Value |
|---|---|
| `iss` | Issuer of the signing proxy/control plane (same identity as ActionPass `iss`). |
| `iat` | Issuance time (seconds). |
| `tenant` | Issuing tenant (optional). |
| `exportDigest` | §3 hash of the **entire canonical export object**. The content binding. |
| `recordCount` | `records.length`. |
| `firstLedgerHash` / `segmentHead` | `ledgerHash` of the first / last exported record (`null` if empty). |
| `sourceLastLedgerHash` | Head of the full source ledger at export time (`export.source.lastLedgerHash`). |
| `range` | The export `filters` (`from`/`to`/`decisions`), so the attested scope is explicit. |

Because the attestation binds `exportDigest` and not the records inline, it stays small and the records travel beside it. Signing keys never leave the issuer; the CLI persists a local ES256 key (PKCS#8) under the ledger directory and publishes only the public JWK — mirroring §10 key custody.

**Attestation bundle (`axtary.ledger_attestation_bundle.v0`).** For portability the export, the attestation token, and the verification key ship as one self-contained artifact: `{ schemaVersion, export, attestation: { token, claims }, publicJwk, keyId }`. This is what `axtary attest-ledger` emits and `axtary verify-export` consumes.

**External verification algorithm.** Given a bundle, an independent verifier — using only a JOSE library and §3 canonicalization — MUST:

1. **Verify the JWS** (`bundle.attestation.token`) with `bundle.publicJwk`: signature valid, `alg` on the allowlist, `typ` = `axtary-ledger-attestation+jwt`, `iss` as expected (if pinned). Recover the claims.
2. **Re-verify each record** in `bundle.export.records`: recompute its `ledgerHash` as the §3 hash of the record without its `ledgerHash` member; mismatch → `ledger_record_hash_mismatch:<n>`. For an unfiltered export, `previousLedgerHash` MUST also chain (§9).
3. **Re-verify the export digest**: recompute the §3 hash of `bundle.export`; it MUST equal `exportDigest` → else `export_digest_mismatch`.
4. **Re-verify approval↔execution equivalence** (§9): for each record whose `executionOutcome` carries the equivalence pair, reject an incomplete pair (`equivalence_pair_incomplete:<n>`), an `executedPayloadHash` that differs from the record's own `payloadHash` (`equivalence_executed_hash_mismatch:<n>`), or `approvedPayloadHash != executedPayloadHash` (`approval_execution_equivalence_failed:<n>`). Pair-free execution records are unproven, not invalid.

A passing bundle proves the export was signed by the issuer key, that no record was edited, reordered, truncated, or appended after signing, and that every approval-bound execution in it executed exactly the approved payload — reproducible by any conformant implementation. The repository's clean-room conformance test (`packages/ledger/src/attestation-conformance.test.ts`) re-implements steps 1–4 from this section without importing the reference hash functions.

### 9.3 Operational timing metadata

An implementation MAY return local operational timing metadata alongside an
authorization result. Axtary's reference proxy reports parse, policy, budget,
ActionPass issuance, decision-ledger append, approval, handler, completed
decision, and total durations. These timings are diagnostics: they are not
ActionPass claims, approval fields, ledger-record fields, or inputs to the
authorization decision, and they MUST NOT be treated as signed security
evidence.

For the reference proxy, the local deterministic decision interval ends only
after the decision record is durably appended and before a provider handler is
invoked. Human approval waits, provider execution, execution-outcome
recording, and optional remote sync are outside that interval.

### 9.4 Transparency-log proofs (RFC 6962 profile)

The §9.1 attestation proves a *whole export* is signed and byte-intact, but it
forces a verifier to read the entire export to check one record and gives no
efficient proof that a later log append-only-extends an earlier one. This
profile adds the RFC 6962 (Certificate Transparency) Merkle tree over the same
per-record commitments.

**Leaves and nodes.** Leaf data for record *i* is the ASCII bytes of its
`ledgerHash`. Because a verifier already recomputes each `ledgerHash` from record
content (§9.1 step 2), binding a leaf to `ledgerHash` transitively binds the full
record content. Hashing uses RFC 6962 domain separation:

- leaf hash = `SHA-256(0x00 || ledgerHash_ascii)`;
- interior node = `SHA-256(0x01 || left || right)`;
- the empty tree hashes the empty string; a single leaf's tree hash is its leaf
  hash. The Merkle Tree Hash (MTH) splits at the largest power of two `< n`.

**Signed tree head.** A `axtary.ledger_attestation.v0` attestation MAY carry two
additional claims that turn it into a signed tree head:

| Claim | Value |
|---|---|
| `merkleRoot` | `sha256:`-prefixed hex MTH over the export's record leaves. |
| `treeSize` | Number of leaves; equals `recordCount`. |

These are optional and additive: older attestations omit them and older verifiers
ignore them; the `exportDigest` still binds the full export. The reference
issuer always emits them.

**Inclusion proof (`axtary.ledger_inclusion_proof.v0`).**
`{ schemaVersion, recordId, ledgerHash, leafIndex, treeSize, merkleRoot,
auditPath[] }`, where `auditPath` is the bottom-up sibling hashes (hex) of the
RFC 6962 audit path. A verifier MUST recompute the leaf from record content,
reconstruct the root from `leafIndex`/`treeSize`/`auditPath` (RFC 6962
§2.1.3.2), and compare it to the signed head's `merkleRoot`. Binding to a signed
head is mandatory: a verifier MUST fail closed (`merkle_root_mismatch`,
`signed_head_missing_merkle_root`) rather than trust a proof's self-declared
root.

**Consistency proof (`axtary.ledger_consistency_proof.v0`).**
`{ schemaVersion, firstSize, secondSize, firstRoot, secondRoot, proof[] }`. A
verifier reconstructs both roots from the RFC 6962 consistency proof (§2.1.4.2)
and MUST bind `firstRoot`/`secondRoot` to two signed tree heads. A proof whose
reconstructed size-`firstSize` root does not equal the earlier signed head proves
the later log rewrote history rather than appending, and MUST be rejected
(`merkle_consistency_invalid`).

Inclusion and consistency proofs are not themselves signed; all trust flows from
the signed tree head. The clean-room conformance test
(`packages/ledger/src/merkle-conformance.test.ts`) re-implements the leaf/node
hashing and both verification algorithms from this section, plus the JWS head
verification, without importing the reference Merkle functions.

### 9.4.1 Cross-issuer verification (`axtary.cross_issuer_trust_root.v0`)

A verifier in another organization can verify one issuer's ActionPass evidence
without receiving the issuer's signing keys or local trust store. This profile is
verify-only: it does not authorize a fresh execution, and it is not a general
OpenID Federation resolver.

The issuer publishes a public trust-root document:

```json
{
  "schemaVersion": "axtary.cross_issuer_trust_root.v0",
  "issuer": "https://issuer-a.example",
  "jwks": { "keys": [] },
  "statusListUri": "https://issuer-a.example/statuslists/actionpasses",
  "federationProfile": {
    "profile": "axtary.openid-federation-anchor.v0",
    "entityStatementTyp": "entity-statement+jwt"
  }
}
```

`jwks.keys` contains public verification keys only. It MUST include the
ActionPass signing key(s), the ledger-attestation signing key(s), and the
status-list signing key(s) needed for the evidence being checked. Signing keys
MUST NOT appear in the trust-root document. `federationProfile` records the
OpenID Federation-compatible typing discipline used by the profile, but this
version pins one configured issuer root rather than resolving arbitrary trust
chains.

Given a pinned trust root, a normalized action, an ActionPass token, a
`axtary.ledger_attestation_bundle.v0`, an `axtary.ledger_inclusion_proof.v0`,
and a freshly fetched status-list token, a cross-issuer verifier MUST:

1. Verify the ActionPass signature against the trust-root JWKS, with `iss`
   equal to the trust-root `issuer`, fixed algorithm allowlist, and the normal
   action/payload binding checks (§8). This audit check does not require a DPoP
   presentation proof because it is not authorizing execution.
2. Verify ActionPass v2 status against `statusListUri` using the trust-root
   JWKS. Missing, unreachable, stale, invalid, revoked, or suspended status
   fails closed.
3. Verify the ledger attestation with a `kid` present in the trust-root JWKS and
   `iss` equal to the trust-root `issuer`.
4. Find a ledger record whose `actionPassId`, `actionHash`, and `payloadHash`
   match the verified pass and action.
5. Verify the inclusion proof for that record against the attestation's signed
   `merkleRoot`; a proof that is not bound to the signed head fails closed.

The success result is an audit report containing the issuer, pass id, record id,
action hash, payload hash, ledger hash, Merkle root, status-list URI, and
verification time. A pass signed by a key outside the trust root, a forged ledger
attestation, a missing included record, or a revoked status is invalid. No token,
approval, policy, ledger, or status-list wire schema changes are introduced by
this profile.

Non-normative reference proof: `npm run demo:cross-issuer` starts two isolated
local Axtary org workspaces, publishes each trust root over HTTP, verifies each
org's ActionPass, ledger attestation, inclusion proof, and live status list from
the other side, and proves a swapped trust root fails. This demonstrates the
profile with founder-operated local parties; it is not an external public trust
network or a network UI.

### 9.5 Forensic reconstruction (`axtary.ledger_forensics.v0`)

Because the ledger records each decision's `actionPassId`, its delegation edge
`{ parentPassId, childPassId, depth, rootPassId }` (§9), its decision/reasons,
and its execution outcome, an auditor can reconstruct a full delegation +
execution incident **from the ledger alone** — no live system, no access to the
passes themselves. The reference offline harness rebuilds the delegation forest
and execution timeline and deterministically asserts three properties:

- **`delegation_attenuation`** — the recorded delegation structure is a
  well-formed, depth-monotonic (`child.depth == parent.depth + 1`), single-root,
  single-parent, acyclic tree. This is the ledger-visible *evidence* of
  attenuated delegation. Strict scope-subset attenuation (a child cannot broaden
  tool/resource/path) is enforced at issuance and verification (§8, delegation),
  not re-derivable from the ledger, which carries action hashes rather than full
  pass scopes — the harness asserts the structural invariants the ledger does
  carry and does not overclaim scope checking.
- **`forensic_reconstructibility`** — every execution outcome carries a
  `correlationId` that resolves to an earlier authorizing (`allow`) ledger record
  for the same pass. Missing, forward, mismatched, or non-authorizing
  correlations are violations; an execution cannot authorize itself merely by
  repeating an `allow` decision on its own outcome record.
- **`cascade_containment`** — once a pass is revoked (a denial record naming it
  with an `actionpass_revoked` reason), neither it nor any descendant in its
  delegation subtree records a later successful execution
  (`post_revocation_success`).

The harness is a deterministic, read-only auditor primitive over verified ledger
records — not anomaly detection, scoring, or monitoring (a §13.2 non-goal). It
reports facts and the specific violations it finds. `axtary forensics` verifies
an attestation bundle's signature, digest, and ledger chain before analysis, or
verifies the local ledger chain before analysis;
`packages/ledger/src/forensics.test.ts` exercises a healthy multi-level incident
plus each violation class.

## 10. Key Distribution and Trust Store

Verifiers hold an `axtary.actionpass_trust_store.v0` document: a set of verification-key records (`kid`, public JWK, optional algorithm restriction, status `active|retired|revoked`, optional validity window) plus revocations (§7). Issuers publish public JWKs out-of-band (file, API, or future JWKS endpoint). Signing keys never appear in the trust store; key custody (KMS, env-scoped dev keys) is an issuer deployment concern. Rotation: publish the new key under a new `kid`, keep the old key `retired` until all passes signed under it have expired (≤ TTL), then drop it.

## 11. Design Decisions

### 11.1 Signing format: JWS/JWT (strict profile) — over PASETO, Macaroons, raw signed JSON

**Chosen: JWS/JWT under RFC 8725 discipline.** Rationale:

- The framing of this spec — token exchange (RFC 8693), DPoP complementarity (RFC 9449), `cnf`-style confirmation (RFC 7800), JWKS-style key distribution — is JOSE-native. Verification works today with every mainstream JOSE library, which is the portability claim this document exists to make.
- The MCP ecosystem's authorization track (OAuth resource metadata per RFC 9728; the active DPoP proposal) is JOSE-based; an MCP-adjacent artifact in a different token family would isolate itself.
- PASETO's legitimate critique of JWT is algorithm agility and `alg:none`. This profile removes that surface rather than inheriting it: fixed allowlist, `typ` enforcement, keyring-resolved keys with per-key algorithm pinning, fail-closed unknown `kid`. (PASETO v4 was the runner-up and would be revisited only if the JOSE alignment requirements above lapse.)
- Macaroons offer elegant attenuation but lack a standard claims vocabulary, mainstream verifier libraries, and audit ergonomics; raw signed JSON re-invents JWS minus the ecosystem.

### 11.2 Hash binding — over payload embedding or full-request signing

Embedding payloads in tokens leaks content into every log that sees a token and bloats the hot path; signing transport requests (à la SigV4) binds to an encoding, not to reviewed content, and breaks across proxies. Hashing the *normalized* payload binds to exactly what policy evaluated and the human approved, independent of transport.

### 11.3 ES256 first

ES256 is FIPS-aligned, universally supported in JOSE stacks, and matches WebCrypto availability for future browser-side verification. EdDSA (Ed25519) is the planned second algorithm; the keyring already carries per-key algorithm metadata to make that additive.

## 12. Test Vectors

Deterministic vectors live in [`spec/test-vectors/actionpass-v0.json`](./test-vectors/actionpass-v0.json), generated by [`spec/test-vectors/generate.mjs`](./test-vectors/generate.mjs) from the reference implementation with a fixed (test-only) ES256 key, fixed timestamps, and fixed identifiers. They contain:

1. A normalized action, with its expected `payloadHash` and `actionHash`.
2. An approval artifact, with its expected `artifactHash`.
3. A signed ActionPass (token + public JWK + expected claims).
4. A two-record ledger chain with expected `ledgerHash` values.
5. Negative cases: a tampered payload (expected reason `payload_hash_mismatch` / `approval_payload_hash_mismatch`) and an expired-time verification (expected `exp` failure).

A conforming independent verifier MUST be able to reproduce all hashes from §3 alone and verify the token using only a JOSE library and §8. The repository's conformance test (`packages/actionpass/src/spec-vectors.test.ts`) does exactly this — it re-implements canonicalization from this document, without importing the reference implementation's hash functions, and checks every vector.

ECDSA signatures are randomized, so token *bytes* are not reproducible across signing runs; vectors therefore ship a specific signed token to verify, not a signature to reproduce.

## 13. Security Considerations

### 13.1 Threat model coverage

| Threat | Counter |
|---|---|
| Stolen/exfiltrated pass replayed for different content | `payloadHash` binding (§8 step 6): the pass authorizes one payload. |
| Payload mutated after human approval (compromised/prompt-injected agent) | Approval binds to `payloadHash`+`actionHash` at artifact validation (§6) and again at verification (§8); mismatch blocks before execution. |
| Pass replayed against a different resource/tool/agent | `aud`, `sub`, tool/resource/owner/runtime/task binding (§8 step 5). |
| Long-lived credential abuse | 600s default TTL; revocation by `jti` within the TTL window (§7). |
| `alg` confusion / `none` / key-type substitution | Fixed allowlist; key+algorithm resolved from the trust store by `kid`, never from the token (§5.1). |
| Unknown or rogue signing key | Fail-closed `kid` resolution against an explicit trust store (§8 step 2, §10). |
| MCP tool-definition swap after approval (tool poisoning) | `toolDefinition.definitionHash` in the action surface (§4): drift changes the action hash and re-enters policy. |
| Ledger tampering (edit/reorder/truncate) | Hash chain (§9); verification fails closed on any break. |
| Sensitive payload content leaking via tokens/logs | Tokens carry hashes, not payloads (§5.3); ledger provider evidence is normalized and sanitized. |

### 13.2 Known limitations (v0)

- **The verifier is trusted.** ActionPass constrains *what* may execute; the component that finally holds provider credentials must actually run §8. Axtary's deployment keeps issuer and executing verifier in the same trusted local proxy, with adapter-side re-verification as defense in depth.
- **No sender-constraint in v0.** A v0 pass intercepted in the local hop could be presented by another party for the same action within its TTL. The implemented v1 profile (`spec/actionpass-v1.md`) closes this with RFC 7800 `cnf.jkt` plus RFC 9449 DPoP.
- **Revocation is local and pull-based** in v0 (§7). The reference proxy and
  MCP wrapper re-read the shared trust store on every presentation, so a local
  revoke takes effect without restart. Cross-host freshness and authenticated
  distribution are not provided by v0.
- **Approval UX integrity** (the human saw what was hashed) is the control plane's responsibility; this spec guarantees binding from artifact onward, not screen-to-artifact.
- **ActionPass does not prevent prompt injection.** It bounds the blast radius of a compromised agent to actions that pass policy unchanged, and makes every attempt attributable in the ledger.

## 14. IANA-Style Registry (informal, v0)

Claim names `apv`, `tenant`, `humanOwner`, `agentRuntime`, `intent`,
`capability`, `decision`, `reasons`, `policy`, `payloadHash`,
`provenanceHash`, `approval`, and `audit` are private claims scoped by
`typ: axtary-actionpass+jwt` and `apv`. Media-type registration
(`application/axtary-actionpass+jwt`) is deferred until the claim set
stabilizes at v1.

## 15. Change Log

- **v0 (2026-06-11)** — Initial public draft, documenting `axtary.actionpass.v0` as shipped in `@axtary/actionpass@0.1.x`. Canonicalization clarified to UTF-16 code-unit key ordering (JCS-aligned); the reference implementation's previously locale-sensitive sort was fixed to match in the same release.
- **2026-06-19** — Added §9.1 Ledger Attestation (`axtary.ledger_attestation.v0` + the `axtary.ledger_attestation_bundle.v0` bundle and the external-verification algorithm), shipped in `@axtary/ledger@0.1.x` and exposed as `axtary attest-ledger` / `axtary verify-export`. Clean-room conformance at `packages/ledger/src/attestation-conformance.test.ts`.
- **2026-06-19** — Added §5.4 policy rule/obligation binding and optional approval `approverRoles`, matching the deterministic multi-rule engine and proxy enforcement shipped in `@axtary/policy@0.1.x`.
- **2026-06-19** — Published the implemented backward-compatible ActionPass v1 proof-of-possession profile in `spec/actionpass-v1.md`, with fixed-input v1 vectors and clean-room DPoP conformance.
- **2026-06-19** — Added normative ActionPass v1 delegation: holder-authorized parent exchange, signed chain depth/root metadata, attenuation-only payload/capability rules, remaining-depth enforcement, child-key `cnf`, and hash-bound ledger edges.
- **2026-06-19** — Activated budget/metering enforcement (§9.2): trusted
  per-tool costs and scoped limits, atomic reservations, over-budget denial,
  commit/rollback evidence, restart reconstruction, and ActionPass budget
  binding. No billing or pricing layer is implied.
- **2026-06-21** — Wired local runtime revocation end to end: durable `0600`
  trust-store records, operator CLI commands, proxy/adapter/MCP checks on every
  presentation, and fail-closed source errors. Clarified that local revocation
  records are not independently signed and that authenticated cross-host
  distribution is outside v0.
- **2026-06-21** — Made the local JSONL ledger append-true and crash-durable:
  inter-process lease locking, tail/head resolution under the lock, in-place
  append, file and directory synchronization, atomic private head checkpoints,
  and stale-checkpoint recovery. The audit record and hash-chain schemas are
  unchanged.
- **2026-06-21** — Defined operational proxy timings (§9.3), including the
  completed local decision boundary. Timing fields remain unsigned diagnostics
  and do not change ActionPass, approval, or ledger schemas.
- **2026-06-21** — Added a durable, shared DPoP replay store for the v1 profile
  (see `actionpass-v1.md` §4.1): a single-file `0600` store under an inter-process
  lease, `fsync`ed and compacted to the in-flight proof window, so a captured
  proof replayed after a verifier restart within its window is rejected
  `dpop_replay_detected`. This is verifier-side replay state only; no token,
  proof, or ledger schema changes.
- **2026-06-21** — Added the RFC 6962 transparency-log profile (§9.4): an
  optional signed tree head (`merkleRoot`/`treeSize`) on the ledger attestation,
  plus `axtary.ledger_inclusion_proof.v0` and
  `axtary.ledger_consistency_proof.v0` artifacts. Verifiers reconstruct roots
  from the proofs and fail closed unless they bind to a signed head. The
  additions are backward compatible — older attestations and verifiers are
  unaffected and the existing `exportDigest` binding is unchanged.
- **2026-06-21** — Documented offline forensic reconstruction (§9.5,
  `axtary.ledger_forensics.v0`): a deterministic auditor harness rebuilds the
  delegation + execution incident from ledger records alone and asserts
  `delegation_attenuation`, `forensic_reconstructibility`, and
  `cascade_containment`, reporting specific violations. Read-only analysis over
  existing record fields; no schema change.
- **2026-06-21** — Published the implemented ActionPass v2 authenticated-status
  profile in `spec/actionpass-v2.md`: signed `{idx, uri}` status references,
  freshness-bounded Token Status List verification, persisted issuer JWKS and
  bounded `kid` rotation, plus optional generic SSF/CAEP session-revocation
  ingestion through an explicit subject-to-root mapping.
- **2026-06-22** — Specified the read-only derived-surface contract for the
  ledger projection (§9): an operator surface MAY reconstruct delegation lineage
  (from the recorded `{ parentPassId, childPassId, depth, rootPassId }` edge) and
  budget state (committed/reserved-vs-limit and denial counts from recorded
  budget events) but MUST NOT display attenuated scopes, infer a limit, or invent
  authority absent from the records. Read-only constraint over existing fields;
  no schema change.
- **2026-06-22** — Specified the tamper-evident **revocation event** (§7): in
  addition to the durable trust-store authority, a revoke records a `deny` ledger
  record naming the pass with `actionpass_revoked`, bound to the revoked action's
  hashes and written after the trust-store revocation (fail toward enforcement).
  Both `axtary revoke` and the local-mode `ledger:revoke` dashboard control
  follow this one path; a hosted dashboard must not present a revoke it cannot
  enforce. No schema change — reuses the existing record shape and the §9.5
  forensics convention.
- **2026-06-23** — Added the M6.6 operator boundary for the existing signed MCP
  provenance fields (§4): publisher keys are trusted explicitly in the local
  enforcement plane; review verifies the exact definition and version-chain
  link before provenance is persisted. Dashboard publisher/semver/chain labels
  are receipt evidence only and MUST NOT create trust or expand ActionPass
  authority. No ActionPass, approval, revocation, budget, status, or ledger
  schema changed.
- **2026-06-23** — Defined descriptor-backed native-connector evidence
  dispatch: exact tool names resolve deterministic sanitizers from the
  connector governance registry; descriptor metadata never grants authority.
  Jira and Linear config/capability/doctor surfaces now derive from that same
  contract. No ActionPass or ledger schema change.
- **2026-06-24** — Hardened the shared local credential broker and OAuth loopback
  (applies to every OAuth connector — Jira/Slack/Linear and remote-MCP OAuth):
  the macOS keychain backend writes the secret over **stdin**, never as an argv
  value (no `ps` exposure to same-user processes); the Dynamic-Client-Registration
  consent binds an **ephemeral** loopback port (no fixed port to pre-bind/race);
  and the OAuth `state` is compared in **constant time**. Storage/credential
  hardening only — no ActionPass, approval, revocation, budget, status, or ledger
  schema changed.
- **2026-06-24** — Added descriptor-backed GitHub inline-review, check-run, and
  issue-comment evidence. Exact targets/content are payload-bound; successful
  writes may record GitHub's non-secret accepted-permission and rate-limit
  response metadata in the existing execution `sideEffectProof`. No ActionPass
  or ledger schema change.
- **2026-06-24** — Added descriptor-backed Postgres read evidence for
  `postgres.query.select`. Policy, evidence normalization, and the adapter parse
  the statement independently; one parameterized SELECT is bound to its exact
  database, statement, parameters, predicate, tables, functions, and row cap.
  The adapter enforces a read-only transaction, least-privilege role, bounded
  timeouts/results, and RLS metadata before execution. Ledger evidence excludes
  the DSN, parameter values, and returned rows. No ActionPass or ledger schema
  version change. **Verified live (2026-06-24)** against real Postgres 16 with a
  dedicated SELECT-only `NOBYPASSRLS` role and an RLS-protected table: a scoped
  read returned only the permitted tenant's rows, a write was refused both by
  policy (`postgres_select_only`, before connection) and by the database role
  (`read-only transaction`), and the exported ledger verified VALID with no DSN,
  parameters, or row values present.
- **2026-06-24** — Added descriptor-backed Google Drive read evidence for
  `drive.files.read`. OAuth uses Google's desktop Picker contract with
  `drive.file` as the only scope, one selected file id returned by the public
  HTTPS callback, and local credential storage. Policy binds the exact
  `drive:file:<fileId>` resource and byte cap; the adapter independently
  requires provider `isAppAuthorized=true` before bounded export/download.
  Ledger evidence excludes access/refresh tokens and file content. No
  ActionPass or ledger schema version change. Live picked-file and unpicked-file
  provider proof remains required before the connector is marked verified.
  **OAuth client requirement (confirmed during live setup, 2026-06-24):** the
  `trigger_onepick` Picker flow needs a custom HTTPS redirect, which only a
  Google **Web-application** OAuth client can register; such clients are
  confidential, so the token exchange MUST present the client secret alongside
  the PKCE verifier. The connect descriptor therefore treats Drive as a
  secret-bearing PKCE client (`AXTARY_DRIVE_CLIENT_ID` + `AXTARY_DRIVE_CLIENT_SECRET`).
- **2026-06-23** — Brought OAuth-2.1-protected **remote MCP upstreams** under the
  govern-any-MCP path (full contract in `spec/mcp-remote-oauth-v0.md`): the
  upstream bearer is obtained by RFC 9728/8414 discovery, RFC 8707
  resource-bound PKCE consent, and optional RFC 7591 registration, and is held
  only in the local credential broker. The governed call still issues the same
  ActionPass and ledger records as any MCP call. The upstream access token is
  custodial credential-broker state, explicitly **outside** the ActionPass,
  approval, revocation, budget, status, and ledger contract — it is never written
  to a pass, the ledger, attestations, or any operator surface. No schema change.
- **2026-06-26** — Documented the reference SDK surface (§16, non-normative): the
  `axtary` facade in `@axtary/actionpass` exposes the five developer verbs
  (`authorize`/`verify`/`record`/`revoke`/`explain`) over a flat request shape,
  delegating to the §5/§7/§8/§9 algorithms unchanged. It is an ergonomic layer,
  not a new artifact — no ActionPass, approval, revocation, budget, status, or
  ledger schema changed.
- **2026-06-26** — Reference SDK §16 dev-key note updated: with no configured key
  the SDK now signs with a **persistent** local dev key (`devKeypair()` —
  `AXTARY_DEV_SIGNING_KEY` else a `0600` keyring file) rather than an ephemeral
  per-process key, so quickstart passes verify across restarts. Non-normative;
  no wire format or schema change.
- **2026-07-06** — Dev-key env access tightened for npm supply-chain hygiene:
  the reference helper no longer reads ambient `process.env`. Env-backed dev
  keys remain supported only when the caller explicitly passes an env map.
- **2026-06-26** — Documented the agent runtime-hook adapters (non-normative):
  the CLI `axtary hook claude-code` and `axtary hook cursor` map an editor's
  pre-action hook payload (Claude Code `PreToolUse`; Cursor `beforeMCPExecution`)
  onto a §4 normalized action, call the same proxy `/authorize` path, and render
  the decision back in the agent's native permission format. They are
  enforcement transports, not a new artifact — the ActionPass, approval,
  revocation, budget, status, and ledger contracts are unchanged. Both fail
  closed in the agent's fail-open zone (an error or unreachable proxy is rendered
  as an explicit `deny`).
- **2026-06-26** — Added the `axtary hook codex` runtime-hook adapter
  (non-normative). It maps a Codex `PreToolUse` payload onto the same §4 content
  actions — a file-touching shell command (Codex's reliable surface),
  `apply_patch` edits, or MCP filesystem tools — and emits Codex's
  `hookSpecificOutput.permissionDecision`. Same enforcement-transport status: no
  artifact or schema change. Codex `PreToolUse` exposes only allow/deny, so a
  step-up is rendered as a fail-closed deny pointing to the payload-bound
  approval path; the raw shell command is never written to the ledger.
- **2026-06-27** — Added the `axtary hook install <claude|cursor|codex>`
  installer for the non-normative runtime-hook adapters. It writes project hook
  config files with resolved local CLI paths and preserves unrelated hook
  settings. This changes adoption ergonomics only: the same hook adapters still
  map agent payloads to §4 normalized actions and call `/authorize`; no
  ActionPass, approval, revocation, budget, status, policy, or ledger schema
  changed.
- **2026-06-27** — Added the PRD §3 scope-template scaffold path
  (non-normative): `axtary init --template <name>` writes a resolved local
  `axtary.yml` plus a policy-test harness for the six reusable scopes
  (`repo-only-coding`, `staging-reads`, `incident-investigation`,
  `ticket-updates`, `doc-search`, and `guarded-prod`), and
  `axtary demo --template <name>` runs the same representative fixtures in
  policy-only mode. This is an adoption/configuration surface over the existing
  §4 normalized actions and deterministic policy decisions; it introduces no new
  ActionPass, approval, revocation, budget, status, policy, or ledger schema.
- **2026-06-27** — Added opt-in OpenTelemetry/OTLP export as a
  non-normative ledger projection: `axtary proxy --otlp-endpoint ...` streams
  payload-free GenAI `execute_tool` spans after durable append, and
  `axtary export-otel --endpoint ...` replays a verified ledger segment to an
  OTLP/HTTP collector. This changes no ActionPass, approval, revocation,
  budget, status, policy, or ledger schema; JSONL + attestation remain the
  verifier contract.
- **2026-06-27** — Release-engineering + CLI presentation pass, all
  non-normative. (1) The `@axtary/*` packages move to a lockstep **0.2.0** with
  internal cross-dependency ranges pinned in step (`scripts/bump-version.mjs`),
  and `@axtary/adapters` now declares `@types/pg` as a dependency so its public
  declarations resolve on a clean install — a packaging-correctness fix with no
  runtime behavior change. (2) The human CLI gains a state-only rendering layer
  (`packages/cli/src/render.ts`): a status-first plain-English verdict, aligned
  fields, and truncated hashes, with color auto-disabled off a TTY / under
  `NO_COLOR` / `--no-color` / `--json`. The verdict prose reuses the existing
  deterministic `explain` reason→prose map, and the `--json` output is byte-for-byte
  unchanged. The OTLP projection's default `service.version` tracks the release
  version. No ActionPass, approval, revocation, budget, status, policy, or
  ledger schema changed; the machine-readable contract is untouched.
- **2026-06-27** — Extended the non-normative CLI rendering layer across the
  remaining decision/result surfaces (postgres-read, github-depth, drive-read,
  the real-workflow run incl. the tamper-outcome beat, demo/template-demo,
  `doctor connectors`, `connect`/`connections`, ledger export/sync/OTLP,
  attestation, `verify-export`, and `init`). The same rule holds: status-first
  verdicts with `explain`-sourced prose, color only on a TTY, `--json`
  unchanged. `doctor connectors` additionally degrades gracefully — a stored
  connector credential that cannot be resolved/refreshed is reported as needing
  setup rather than crashing the readiness check. Presentation + a fail-safe
  readiness fix only; no ActionPass, approval, revocation, budget, status,
  policy, or ledger schema changed.
- **2026-06-27** — Completed the non-normative CLI presentation pass and added
  truthful async progress (mega-plan M8.9/M8.10). The remaining human formatters
  (`hook install`, `policy simulate/test`, `test-policy`/`--parity`,
  `mcp review/conformance/drift-demo`) now render status-first; `status`/`caep-map`
  stay JSON-only. `axtary smoke` animates a branded spinner on a TTY **stderr**
  stream for each genuine network check and resolves to an ok/fail line on
  **stdout** — progress on stderr, durable result on stdout, no animation into
  pipes/CI. A dedicated CLI e2e + error-path suite asserts the quickstart runs
  end to end, `--json`/piped output is ANSI-free and parseable, `--no-color` is
  honored, and every error path fails closed with a one-line stderr message and
  no leaked stack trace. Presentation + progress only; no ActionPass, approval,
  revocation, budget, status, policy, or ledger schema changed; the
  machine-readable contract is untouched.
- **2026-06-27** — Added the hosted interactive playground and runtime hook
  bypass-resistance coverage as non-normative adoption/verification surfaces.
  `/playground` and `/api/playground` run deterministic scenarios through the
  existing ActionPass hash helpers, approval-artifact validation, policy
  evaluator, ledger record helper, MCP definition-hash policy, Postgres SELECT
  analysis, and runtime-hook normalization. Claude Code hook coverage now also
  maps file-touching `Bash` commands to §4 `github.contents.read/write` actions,
  sharing the conservative shell classifier used by Codex. This closes the
  prompt-level "ignore the hook" bypass for covered file actions: the model's
  instruction is not authority; the intercepted tool call is. No ActionPass,
  approval, revocation, budget, status, policy, or ledger schema changed.
- **2026-07-01** — Deepened the shared runtime-hook shell classifier
  (`shell-file-normalize.ts`) so a single command that performs multiple file ops
  is mapped to multiple §4 actions, each authorized independently, with the hook
  returning the **most restrictive** decision (deny > step-up > allow).
  Copy/move commands (`cp`/`mv`/`install`/`rsync`), `dd if=/of=`, `tar` create,
  and reader-with-redirection (`cat secret > public`) now govern the **source
  read** as well as the destination write — closing the `cp secret dest`
  exfiltration gap documented as a known limit in §13.2. The classifier is
  pipeline-aware to avoid false positives (a downstream `… | grep PATTERN` reads
  stdin, so its operand is not treated as a file). Non-normative hook-adapter
  behavior only; no ActionPass, approval, revocation, budget, status, policy, or
  ledger schema changed.
- **2026-07-06** — Reason-precision pass on approval-artifact validation (§6):
  the `payloadHash` binding check now runs before the `actionHash` check, so a
  swapped payload — which necessarily changes both hashes — reports the precise
  `approval_payload_hash_mismatch` instead of the generic action mismatch,
  matching the §8 step-6 verification-time claim order. Both checks still run
  and every failure remains fatal (no pass). Enforcement proxies additionally
  validate a resolved approval artifact against the action **at approval
  time**, so a stale artifact denies with the bare §6 reason in the ledger
  rather than a wrapped issuance-failure reason, and the deny reasons also
  carry the approval's bound hash as a `approved_payload_hash:sha256:…` detail
  — making the deny record self-contained mismatch evidence (approved hash in
  the reasons, attempted hash as the record's `payloadHash`, and the mismatch
  reason). Issue-time validation is unchanged as the fail-closed backstop.
  Reasons remain free strings; no token, approval, policy, or ledger schema
  changed.
- **2026-07-07** — Approval↔execution equivalence proof (§9, §9.1 step 4):
  `axtary.ledger_execution_outcome.v0` gains **optional**
  `approvedPayloadHash` + `executedPayloadHash`, stamped by the runtime
  whenever an executed action carried approval evidence, making
  `approved_payload_hash == executed_payload_hash` a first-class, queryable,
  exportable proof. Export verification MUST reject an incomplete pair, an
  executed hash disagreeing with the record's hash-chained `payloadHash`, or
  approved ≠ executed; pair-free executions are unproven, never fake-proved
  and never invalid. Added `axtary prove-equivalence` (per-record proof
  entries + summary, `--out` exportable, non-zero exit on any failed pair) and
  an equivalence summary to `verify-export`; the clean-room conformance
  verifier re-implements the new step. Fields are additive — existing
  records, exports, and verifiers are unaffected; no token, approval, or
  policy schema changed.
- **2026-07-08** — Non-normative positioning update for M9.3: public and PRD
  language now leads with **content authorization** for AI-agent actions —
  approval bound to the exact diff, message, query, or MCP tool call — rather
  than generic runtime-authorization phrasing. This is a naming/framing change
  only; the §4 normalized action, §6 approval binding, §8 verification, §9
  ledger/equivalence checks, and every wire/schema contract are unchanged.
- **2026-07-08** — Added the M9.4/M9.5 standards-conformance surfaces without
  changing ActionPass, approval, policy, or ledger schemas. The Agent
  Control-style shim (`spec/agent-control-shim-v0.md`) maps
  `pre_tool_call`/`post_tool_call` snapshots into §4 normalized actions and
  returns fail-closed ACS-style verdicts; `axtary acs conformance` proves the
  allow path through ActionPass issuance and ledger append. `test-policy
  --parity` now also emits an offline AgentCore Cedar-shaped Gateway request
  mapping, while native/Cedar/Rego decisions remain the executable parity gate.
  The MCP SEP draft is surfaced publicly as an Axtary draft, not an accepted MCP
  standard.
- **2026-07-08** — Added the M9.6 cross-issuer verification primitive (§9.4.1):
  `axtary.cross_issuer_trust_root.v0` publishes an issuer, public JWKS,
  status-list URI, and OpenID-Federation-compatible typing marker; the reference
  verifier checks an ActionPass, signed ledger attestation, inclusion proof, and
  fresh status evidence against that pinned root. The reference local proof
  starts two isolated org instances, exchanges HTTP-published roots both ways,
  and proves a swapped trust root fails. This is verify-only and does not
  implement arbitrary OpenID Federation trust-chain resolution. No ActionPass,
  approval, policy, ledger, or status-list wire schema changed.
- **2026-07-08** — Public publication note for M9.7: the reference app serves
  this canonical markdown draft at `/spec/actionpass-v0.md` and documents the
  standalone verifier path at `/docs/actionpass-spec`. This is a publication
  surface only; no ActionPass, approval, policy, ledger, verifier, or
  status-list wire schema changed.
- **2026-07-08** — M9.8 reference-implementation supply-chain hygiene
  (non-normative, no wire change): every reference library package now
  requires a caller-injected fetch-compatible transport (and, for connector
  readiness, a caller-passed env map) and fails closed with a named reason —
  `otel_export_fetch_required`, `ledger_sync_fetch_required`,
  `approval_queue_fetch_required`, `mcp_http_fetch_required`,
  `mcp_oauth_fetch_required`, `connector_readiness_env_required` — matching
  the status-list retrieval contract already recorded on 2026-07-06. The CLI
  and hosted app supply the transport at their application boundary. No
  ActionPass, approval-artifact, policy, ledger, status-list, or verifier wire
  schema changed. The reference CLI demo additionally accepts
  `--approve-step-up`, which resolves step-up decisions through the existing
  §6 approval-artifact contract (exact payload-hash binding; the execution
  record carries the §9 approved/executed hash pair).

## 16. Reference SDK surface (non-normative)

This section is **non-normative**. It documents how the reference TypeScript
SDK (`@axtary/actionpass`) exposes this artifact to developers; it defines no
new wire format and changes no schema in §4–§10.

The PRD names five required SDK functions. The `axtary` facade implements them
over a single flat request shape (`agent`, `human`, `intent`, `tool`,
`resource`, `payload`), which it maps to a §4 normalized action and delegates
to the algorithms already specified here:

| Verb | Delegates to | Result |
| --- | --- | --- |
| `authorize(request)` | §5 issuance + §9 ledger record | A decision (`allow`/`deny`/`step_up`); an allow issues a payload-bound `axtary.actionpass.v0` pass, a deny/step-up issues none. |
| `verify(pass, action)` | §8 verification algorithm | Valid only for the exact presented action; fails closed on `payload_hash_mismatch` and the other §8 reasons, including `actionpass_revoked`. |
| `record(result)` | §9 ledger record (execution outcome) | A tamper-evident record chained after the decision record. |
| `revoke(passId)` | §7 revocation | An `axtary.actionpass_revocation.v0` record that makes a later `verify` of the pass fail closed. |
| `explain(decision)` | reason → prose map | A deterministic, model-free explanation (`label`, `reviewerAction`, `summary`, per-reason detail). |

Key custody is an issuer deployment concern (§10). With no key configured the
reference SDK signs with a **persistent local dev key** so the quickstart runs
without setup: it resolves an ES256 key from a `0600` local keyring file
(`devKeypair()`), surviving process restarts. Env-backed dev keys remain
available only when the caller explicitly passes an env map to the helper; the
package does not read ambient `process.env` by default.
This is a development convenience only — production deployments supply an issuer
key or a hosted issuer per the §10 rotation/custody guidance. Durable ledger
persistence, configurable policy files, and credential isolation remain proxy
concerns; the SDK and proxy emit the same artifacts.
