approval-md 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
package/SPEC.md
CHANGED
|
@@ -127,11 +127,13 @@ channels:
|
|
|
127
127
|
|
|
128
128
|
**An absent `approval_ttl` declares that nothing lapses.** `defaults.approval_ttl` is optional, and its absence is a statement rather than an omission for a runtime to fill in. Under a policy that omits it, a pending request stays actionable until a human decides it, the execution token a grant mints stays spendable until it is consumed or revoked (§10.4), and a harness grant stays carryable on the same terms. Implementations MUST NOT invent a default duration: a number the policy never wrote would expire approvals its author never asked to expire, and a runtime that instead read the absence as an immediate lapse would refuse every decision under a policy whose only fault is omitting an optional key, leaving no recovery path through its own gate. Where the key IS declared, the lapse is measured from the `approval.requested` timestamp, bounds the pending request and the token its grant mints alike, and is judged at decision time whether or not an `approval.expired` record has been written; an instant that cannot be parsed at either end reads as lapsed, because liveness that cannot be demonstrated is not assumed. `on_expiry` governs only how a lapse that a declared TTL produced is projected. A policy that fails to load carries no TTL either, so its fail-closed all-`manual` resolution comes with no deadline: failing closed raises the scrutiny an action gets, and it does not shorten the time a human has to answer. (Amended APRV-137.)
|
|
129
129
|
|
|
130
|
+
**Explicit irreversible-action autonomy.** A class rule MAY set `allow_irreversible: true` alongside `autonomy: autonomous`, `supervised`, `supervised-retro`, or `supervised-live`. For example, `communicate.zzz.external: { autonomy: supervised, allow_irreversible: true }` retains retrospective supervision when its action truthfully declares `reversible: false`. The key is optional and boolean; omission or `false` preserves the manual floor of §7. `true` on `manual` or `human-only`, a non-boolean value, or the key in `defaults` is schema-invalid and fails closed. A manual rule needs no override. (Amended APRV-317, pending sign-off.)
|
|
131
|
+
|
|
130
132
|
### 5.2 Policy semantics
|
|
131
133
|
|
|
132
134
|
- **Matching.** Classes match most-specific-first; `*` is a single-segment wildcard, a trailing `.*` matches any depth. An action whose class matches no rule takes `defaults.autonomy`. Implementations MUST fail closed: unparseable policy means everything is `manual`. A trailing `.*` consumes one or more segments: `read.*` matches `read.web` and `read.web.page`, and it does not match the bare class `read`. The schema admits `read` and `read.*` as two distinct keys a policy may list separately with different autonomy, so neither is an alias of the other, and a policy that wants the bare class covered writes it as its own rule or leaves it to `defaults.autonomy`. A bare `*` is a single-segment wildcard rather than a trailing one, so it matches any one-segment class and nothing deeper, and `*.*` matches any class of two or more segments. (Amended APRV-137.)
|
|
133
135
|
- **Specificity.** Pattern specificity is compared as follows: (1) more literal (non-wildcard) segments is more specific; (2) ties broken by fewer wildcard segments; a trailing `.*` counts as a single wildcard segment and contributes no literal segments. Patterns still tied are equally specific and the strictest-autonomy rule applies. An earlier criterion (3) broke remaining ties by greater total segment count; it was removed because it could never fire. Every segment is either literal or wildcard, so two patterns tying on (1) and on (2) have equal totals by arithmetic, and a tie surviving both criteria is genuine equality. (Amended APRV-136.)
|
|
134
|
-
- **Deny beats allow.** If multiple rules match at equal specificity, the strictest autonomy wins (`human-only` > `manual` > `supervised-live` > `supervised-retro` > `autonomous`). That is the strictness ordering, and it binds everywhere an ordering is consulted: this tie-break, any comparator an implementation derives from it, and any surface that ranks or sorts levels. `human-only` heads it because it is strictly more scrutiny than `manual`: `manual` says a human decides and an agent then acts, `human-only` says the human acts, so a tie between the two resolves to the level under which no agent executes. (Amended APRV-185.) The declared `live_rate` is not part of the ordering: two equally specific `supervised-live` rules that disagree about a fraction are equally strict, and the tie falls through to the implementation's deterministic tie-break, so an author cannot move a rule's precedence by tuning a number. (Amended APRV-127.) Resolution selects
|
|
136
|
+
- **Deny beats allow.** If multiple rules match at equal specificity, the strictest autonomy wins (`human-only` > `manual` > `supervised-live` > `supervised-retro` > `autonomous`). That is the strictness ordering, and it binds everywhere an ordering is consulted: this tie-break, any comparator an implementation derives from it, and any surface that ranks or sorts levels. `human-only` heads it because it is strictly more scrutiny than `manual`: `manual` says a human decides and an agent then acts, `human-only` says the human acts, so a tie between the two resolves to the level under which no agent executes. (Amended APRV-185.) The declared `live_rate` is not part of the ordering: two equally specific `supervised-live` rules that disagree about a fraction are equally strict, and the tie falls through to the implementation's deterministic tie-break, so an author cannot move a rule's precedence by tuning a number. (Amended APRV-127.) Resolution selects one winning rule for autonomy, rates, approvers and limits. The explicit irreversible-action capability below is the sole exception. Among rules tied on the full specificity key the strictest autonomy wins; among rules tied on both specificity and strictness the lexicographically smallest pattern wins, so the outcome is deterministic and independent of the policy file's key order. The winning rule's `approvers` and `limits` govern, and the `approvers` and `limits` of every other matching rule are discarded. Implementations MUST NOT union, merge, or intersect the approver sets of tied rules, and MUST NOT apply a limit declared by a rule that did not win: class-scoped consumption is attributed by the winning rule's own pattern (see **The budget moment** below), so a ceiling taken from a different pattern would be compared against a window it does not scope. An author who wants several ceilings over one class writes them on the one rule that governs it. (Amended APRV-137.) For `allow_irreversible`, every matching rule tied at maximum specificity MUST explicitly set `true`; omission or `false` in any member denies the capability, irrespective of strictness or lexical tie-break. Lower-specificity matches do not participate. Defaults and failed policy loads never supply it. Where a protected `policy.edit` child inherits its parent resolution, it inherits this capability too. Protected-route floor checks MUST compare both ordinary resolution and effective resolution with `reversible: false`, as well as the existing supervision-rate constraints, so a routed child cannot weaken a protected parent through this key. (Amended APRV-317, pending sign-off.)
|
|
135
137
|
- **`approvers` binds the grant.** A class rule's `approvers` list names the people who may authorize actions of that class, and it MUST be enforced at the moment of the grant: a grant recorded by a `human:` actor the winning rule's `approvers` does not name is refused with its own reason, `actor-not-approver`, distinct from `actor-not-human`, which says the actor is not a person at all. The distinction is the repair. One says run the verb as a person; the other says ask a person the policy put in front of this class. The check binds `grant` alone. Rejection and revocation withdraw authority rather than confer it, so restricting them would leave a request standing, or an authorization live, because the wrong person tried to end it. A rule that declares no `approvers` restricts nobody, since the list is a narrowing and a narrowing nobody wrote narrows nothing; a resolution taken from `defaults.autonomy`, or produced by a failed load, carries no list for the same reason, which keeps a repository with a broken policy recoverable through its own gate and leaves attestation as the control on that path. An entry matches an actor when it equals the actor's identifier with the `human:` prefix removed, and matching is exact, with no case folding and no other normalization. The schema requires at least one entry, so a valid policy cannot declare an empty roster; where an implementation admits one it names nobody and refuses every grant. The list survives the §7 irreversibility floor, which raises autonomy and changes no roster. The control sits inside the trust boundary §11 states plainly: human identity in v0.1 is config-declared, so this defends against the wrong approver answering, and it does not defend against an actor choosing whose name to wear. (Amended APRV-137.)
|
|
136
138
|
- **The budget moment.** Exactly two event types consume budget. `approval.granted` is the authorizing moment of the manual path, and `execution.started` is the authorizing moment of the supervised and autonomous paths, which emit no approval event at all (§6.3). An `execution.started` consumes only where the same window holds no `approval.granted` bearing the same `action_key`, so a manual action that is granted and then started is charged once. Nothing else consumes: `task.registered` declares rather than authorizes, and every rejection, expiry, revocation, withdrawal and outcome event reports on a commitment already charged at authorization, or on one that was never made. A consuming event whose `est_cost_usd` is unusable contributes nothing to money limits and still counts as one action against action-count limits, since an authorization that declared no cost is still an authorization. A consuming event whose `payload.class` is unusable is invisible to class-scoped limits, which cannot show it belongs to the class, and is still charged by every global budget, which charge every authorization whatever its class. Class-scoped consumption is attributed by the winning rule's pattern rather than by string equality with the action's class, so one `financial.*` rule is one ceiling shared by every class it governs. The envelope cap of §6.2 is a lifetime total over the task's own authorizations rather than a rolling window, and it binds at intake, at grant, and at execution start, because a cap enforced at one door is a cap a caller routes around by choosing another. The double-charge guard above is scoped to the window: where a grant has aged out of the window and its `execution.started` has not, the start consumes, so an action straddling two windows is charged in both. That is the direction that charges more, and a ceiling an author could cross by waiting would be no ceiling. (Amended APRV-137.)
|
|
137
139
|
- **The autonomy split: `supervised-live` and `supervised-retro`.** `supervised` names two different bargains, and a policy author is entitled to choose between them. A **`supervised-retro`** class executes immediately and a fraction is escalated for after-the-fact review, exactly as `supervised` always did. A **`supervised-live`** class declares a `live_rate` in (0, 1]: that fraction of its actions BLOCK on the human gate before executing, following the `manual` path in full — same request, same prompt, same grant, same token rules — and the remainder proceed and stay eligible for retrospective review. The bare `supervised` is an alias of `supervised-retro`, so every policy written before the split keeps the meaning its author gave it; implementations MUST record a load-time note wherever the alias is used rather than reinterpret it silently. `live_rate` is REQUIRED for `supervised-live` and forbidden elsewhere, and `defaults.autonomy` MUST NOT accept `supervised-live`, which has nowhere to declare a rate. Live selection reuses the `supervised_sample_rate` construction below and MUST NOT invent a second one: selected iff HMAC-SHA-256 over the action's `payload_hash`, keyed by the operator-held secret named by `audit.sampling_secret_env`, falls under the rate. Keying on the payload hash rather than on a nonce, a timestamp, or an attempt counter is what removes the re-roll: identical bytes always select identically, so a refused or retried request cannot ask again until it comes up unsampled, and the only way to change the verdict is to change the action. No selection value and no marker of the draw enters the log. A value would be an oracle for a secret agents must not hold; a marker would be a distinction an approver could act on, and the whole worth of the mechanism is that a sampled action is answered exactly as a manual one is. Nothing is lost to audit, because an operator holding the secret can recompute any verdict from the request's own `payload_hash` and the `live_rate` of the policy that request pinned in `policy_sha256` — evidence stronger than a self-reported field, since it cannot be forged by whoever wrote the record. Unlike retrospective sampling, live selection **fails closed**: with no usable secret every action in the class is gated, because "gate all of them" is fully specified and needs no seed. An action whose declaration carries no `payload_hash` has nothing to draw over and is gated for the same reason. (Amended APRV-127.)
|
|
@@ -143,12 +145,12 @@ channels:
|
|
|
143
145
|
- **`supervised_sample_rate`** implements auditing under a budget: supervised actions execute immediately, and a deterministic pseudo-random sample is queued for after-the-fact human review. Sample selection MUST be reproducible by the operator and unpredictable to agents. Implementations derive the selection value as HMAC-SHA-256 over the event hash using an operator-held secret stored outside the repository and outside any agent-readable path. Seeding from event content alone is forbidden: the authoring agent controls that content and could search it until its action falls outside the sample. See §11 for why. The secret is configured by name: `audit.sampling_secret_env` holds the *name* of the environment variable the operator keeps it in, so the policy file an agent may read carries a variable name and never the secret itself. The secret MUST NOT appear in the log, in any command's output, or in any error message. When the rate is absent or zero, or the secret is unnamed or unset in the runtime's environment, **sampling is disabled and the runtime states the reason in machine-readable form**. This is the one place the runtime does not fail closed, and the reason is that the strict reading is unavailable: escalating everything would require a selection function, the only remaining seed is the event content, and a content-seeded sample is one the authoring agent can steer. A control that looks like it is running while being steerable by the party under oversight is worse than a control that is visibly off. Sampling is retrospective, so a disabled sampler costs review coverage and authorizes nothing: no gate consults it and no budget is spent by it. (Amended APRV-40, which implements the sampler in the M5 daemon.) **The rate may be declared per class.** A class rule MAY carry `retro_rate`, a proportion in (0, 1] that governs the retrospective sample for that class alone; a class declaring none is sampled at `audit.supervised_sample_rate`, so a policy written before this key behaves exactly as its author configured it. One review budget rarely fits every class: a repository may want one action in a hundred of its chattiest supervised class reviewed and every one of a small, consequential class. `retro_rate` is admitted on `supervised`, `supervised-retro` and `supervised-live`, the last because the fraction a live draw does not gate executes and stays in the retrospective pool. It is a schema violation on `manual` and `autonomous`, which have no retrospective pool at all, and the policy therefore fails closed rather than carrying a review fraction nothing reads, exactly as `live_rate` fails on the levels it does not belong to. Selection under a class rate is the construction above, unchanged: HMAC-SHA-256 over the event hash, the same operator-held secret named by `audit.sampling_secret_env`, one mechanism. Only the threshold the value is compared against moves, so identical bytes still select identically and an operator holding the secret can still recompute any verdict, now against the rate the record itself states. The disabled-sampler rule is per class on the same terms: a class whose rate is absent or zero, or whose secret is unnamed or unset, is not sampled, the reason is machine-readable, and the runtime's health surfaces report which classes sample at which rate and which sample at nothing. A global rate that is absent while some class declares its own leaves those classes sampling and the rest uncovered, and saying so plainly is the whole of the honesty rule: coverage an operator believes they have and do not is the failure this bullet exists to prevent. (Amended APRV-183.)
|
|
144
146
|
- **Request-volume limits.** `limits.max_pending` (per class or global): maximum simultaneously pending requests; further requests are refused at intake with reason `queue-full`. `limits.requests_per_hour` (per origin): rolling-window ceiling on request creation; excess is refused with reason `rate-limited` and logged. Defaults are generous; the purpose is a tripwire against queue-flooding and approver fatigue, which attack the human's attention rather than the action budget. Refused requests consume no budget and no attention beyond the log line. (Policy vocabulary in v0.1; enforcement lands with M4/M5.)
|
|
145
147
|
- **Agents MUST NOT be able to modify `APPROVAL.md`, the log, or channel credentials.** The reference runtime refuses to mint tokens if the policy file's mtime/hash changed without a signed `policy.updated` event from a human session. Attestation makes this mechanical: a human runs `approval policy attest`, which appends a `policy.updated` event carrying the SHA-256 of the policy file's bytes. Gate operations — request intake, grant recording, token minting — MUST refuse, with a distinct machine-readable reason, whenever the live file's hash differs from the latest attestation or no attestation exists. An edited policy is inoperative until a human re-attests it. Attestation also names the rules a decision was made under: gate-written `approval.requested` and `approval.granted` events carry `policy_sha256`, the attested hash in force when the runtime evaluated them. The field is assigned at the write boundary exactly as `ts` is (§8): no caller can supply it, and a value arriving from outside the runtime is refused. When the hash in force at grant time differs from the hash recorded on the matching request, the grant MUST refuse with its own reason, `policy-drift`, distinct from the unattested-file refusal above: the file is attested and is a different policy, so the pending request is void and must be re-requested under the rules now in force. The field is additive per §8: records written before it existed still validate and verify, and a verifier accepts both forms. (Amended APRV-118.)
|
|
146
|
-
- **Attestation of the gate's organs.** The organs are the harness files that install the enforcement hook (`.claude/settings*`, `.cursor/hooks.json`, `.cursor/hooks/`, `.cursor/agents
|
|
148
|
+
- **Attestation of the gate's organs.** The organs are the harness files that install the enforcement hook (`.claude/settings*`, `.cursor/hooks.json`, `.cursor/hooks/`, `.cursor/agents/`, `.codex/hooks.json`, `.codex/hooks/`, `.codex/config.toml`): the `policy.core` surface outside the approval home and outside the policy file itself. A human attests one by content, exactly as they attest the policy file, with `approval policy attest --organ <path>`, which appends a `gate.organ.attested` event carrying the file's repository-relative path and the SHA-256 of its bytes. The verb is human-only, one path per call, and the digest is computed by the runtime from the file on disk: a caller who could supply the hash could attest bytes nobody read. The organs need this because they are `policy.core` and a policy may resolve `policy.core` to `human-only`, in which case the gate mints no record of any kind for a change to one (§11.1 invariant 9), so grant-shaped evidence for a hand edit cannot exist however carefully the edit was made. What reads these records is the after-the-fact enforcement of §10.1: a checker requiring evidence that a human saw a protected change accepts an organ whose bytes at the commit under review hash to a digest attested FOR THAT SAME PATH, and a digest attested for another path is not evidence. What does NOT read them is the gate: an organ attestation MUST NOT make an unattested policy operative, MUST NOT alter the `policy_sha256` a request or a grant is decided under, and MUST NOT satisfy any check about the policy file. Implementations SHOULD discharge that structurally, by giving the organ record its own event type, rather than by filtering a shared one in every reader. (APRV-272.)
|
|
147
149
|
- **`payload_retention`.** An optional top-level duration bounding how long the payload bytes in `.approval/payloads/` (§9) are kept. A payload is prunable once the action it is bound to has been in a terminal state (`executed`, `rejected`, `expired`, `revoked`) for longer than the duration. A payload whose action is not terminal is never prunable, at any age: a pending or granted approval binds to those exact bytes, and discarding them would leave a live authorization pointing at nothing. When the key is present, orphaned payloads (bytes with no recorded binding) are prunable at any age: the duration governs bound payloads and does not gate residue nothing ever bound. When the key is absent, the pruning subsystem does not run and nothing is deleted, orphaned or not; the store holds the material evidence of what a human approved, so forgetting anything is an operator's explicit choice, and an operator who never made that choice never asked the runtime to delete anything (amended APRV-49 to match the enforcement shipped in APRV-41). Pruning is performed by the daemon and by nothing else, and each removal appends a `payload.pruned` event, so a log states what its store no longer holds. (Policy vocabulary in v0.1; enforcement lands with the M5 daemon.)
|
|
148
150
|
- **`protected_paths`.** An optional top-level list widening the set of files whose edit is classified `policy.edit`, so a project can put its own governing documents (a specification, a constitution, a design directory) behind the gate that already stands in front of `APPROVAL.md`. Entries are repo-relative and literal: an exact file path (`SPEC.md`, `docs/constitution.md`) or a directory prefix ending in `/` (`design/`). Globs, negation, absolute paths and `..` segments are schema violations, because a pattern language the runtime half-implemented would leave an author believing a file is gated when it is not. Matching is by path segments and never resolves against a checkout, so a linked worktree and the primary answer alike: an exact path matches a candidate whose trailing segments are that path (a single-segment entry therefore matches that filename in any directory, exactly as the built-in filenames do), and a directory prefix matches a candidate containing those segments as a contiguous run. The key is ADDITIVE and can only widen: the runtime's built-in protected set (the policy file, the agent instruction files, the approval home, the harness settings, the release configuration) stays protected whatever this list says or omits, and a policy that fails to load leaves those built-ins in force while every class resolves to `manual`. (Amended APRV-107.) Every path a policy adds is `policy.edit`: a policy widening its own protected surface is naming prose and configuration, and cannot mint authority over the gate's organs, whose classes the runtime fixes. (Amended APRV-198.)
|
|
149
151
|
- **`vault.passphrase_env`.** An optional top-level key naming the environment variable that holds the passphrase for the credential vault of §10.4. The policy carries the variable's *name*, never the passphrase, on the same reasoning as `audit.sampling_secret_env` and the channel credential keys (`chat_id_env`, `token_env`): agents may read `APPROVAL.md`, and a passphrase they can read is a vault they can open. The passphrase MUST NOT appear in the log, in any command's output, or in any error message. When the key is absent the runtime reads `APPROVAL_VAULT_PASSPHRASE`; a variable name is not a permission, so an unnamed one MUST NOT lock an operator out of credentials they created, and a policy that fails to load leaves the default in force for this key alone. (Amended APRV-68, which implements the reference vault.)
|
|
150
152
|
- **`channels.telegram.token_env` and `channels.telegram.chat_id_env`** are honoured by the runtime exactly as `audit.sampling_secret_env` and `vault.passphrase_env` are: the policy carries the variable's *name*, the runtime reads the value from the environment under that name, and a policy that declares neither (or fails to load) gets the reference runtime's defaults, `APPROVAL_TG_TOKEN` and `APPROVAL_TG_CHAT`. (Amended APRV-72.)
|
|
151
|
-
- **The environment map.** Every key above that ends in `_env` carries a variable's name rather than its value, which leaves an operator with several values to establish before any gate operation works and nowhere to record where those values live. `.approval/env` is that place: a SOURCE MAP, sibling of the log directory as the vault is, one `KEY=VALUE` per line with `#` comments and blank lines ignored, no quoting and no interpolation. The VALUE says where the value lives, in one of four forms: `keychain:<service>` (macOS, `security find-generic-password -a "$USER" -s <service> -w`), `secret-service:<label>` (Linux desktop, `secret-tool lookup approval <label>`), `env:` (inherited from the ambient environment, and reported as inherited), or a bare literal. A literal is permitted and is ALWAYS reported as plaintext, by every diagnostic, because a rule people route around is not a control: an operator told plainly that their token sits in a file in the working tree can weigh that, while an operator forbidden from writing it there writes it into a shell profile where nothing can see it to say so. Values are never passed to a helper in an argv; they arrive on its stdout. The file MUST be mode 0600 (anything else is refused, with the `chmod` to run) and `approval init` adds it to `.gitignore`. **No command loads this file implicitly. A single verb, `approval env`, resolves it and emits an export block for a shell to evaluate, so the environment a gate operation runs under is always one a human established** (§11, §11.1 invariant 7). An already-exported value always wins over the file, and an absent file is not an error. The variables answered for are the human identity variable, the two Telegram variables, the vault passphrase, the sampling secret when the policy names one, and any other string-valued `_env` key in the policy. (Amended APRV-73.)
|
|
153
|
+
- **The environment map.** Every key above that ends in `_env` carries a variable's name rather than its value, which leaves an operator with several values to establish before any gate operation works and nowhere to record where those values live. `.approval/env` is that place: a SOURCE MAP, sibling of the log directory as the vault is, one `KEY=VALUE` per line with `#` comments and blank lines ignored, no quoting and no interpolation. The VALUE says where the value lives, in one of four forms: `keychain:<service>` (macOS, `security find-generic-password -a "$USER" -s <service> -w`), `secret-service:<label>` (Linux desktop, `secret-tool lookup approval <label>`), `env:` (inherited from the ambient environment, and reported as inherited), or a bare literal. A literal is permitted and is ALWAYS reported as plaintext, by every diagnostic, because a rule people route around is not a control: an operator told plainly that their token sits in a file in the working tree can weigh that, while an operator forbidden from writing it there writes it into a shell profile where nothing can see it to say so. Values are never passed to a helper in an argv; they arrive on its stdout. The file MUST be mode 0600 (anything else is refused, with the `chmod` to run) and `approval init` adds it to `.gitignore`. **No command loads this file implicitly. A single verb, `approval env`, resolves it and emits an export block for a shell to evaluate, so the environment a gate operation runs under is always one a human established** (§11, §11.1 invariant 7). An already-exported value always wins over the file, and an absent file is not an error. The variables answered for are the human identity variable, the two Telegram variables, the vault passphrase, the sampling secret when the policy names one, and any other string-valued `_env` key in the policy. (Amended APRV-73.) The explicitly invoked human-only quickstart ceremony (§10.1) MAY resolve the declared sources of the fresh instance it just created solely for its health-preflight child. This is an explicit setup operation, not ambient loading by a gate, listener, agent tool or later process. It MUST NOT import another instance's ambient approval credentials, change the calling shell, or attest bytes that were not shown and confirmed. The operator still establishes the later gate environment through `approval env`. (Amended APRV-309, pending sign-off.)
|
|
152
154
|
- **Durations.** Every duration-valued field (`approval_ttl`, budget windows, `max_latency`, `payload_retention`) is a string matching `<positive integer><unit>` with unit one of `ms`, `s`, `m`, `h`, `d`, `w` (weeks = 7 days). Single unit only: compound (`1h30m`), fractional (`1.5h`), zero, and leading-zero forms are invalid. An invalid duration anywhere in the policy is a schema violation and the policy fails closed.
|
|
153
155
|
|
|
154
156
|
### 5.3 The values block
|
|
@@ -287,11 +289,13 @@ The `policy.*` split is by consequence rather than by file type. `policy.edit` i
|
|
|
287
289
|
|
|
288
290
|
`files.delete.out_of_scope` (destructive deletes outside the task's stated scope, inside the workspace) sits under the existing `files` namespace at manual; `data.delete` remains the class for deletes outside the workspace.
|
|
289
291
|
|
|
290
|
-
Two invariants: an action
|
|
292
|
+
Two invariants: an action must declare its class before requesting an execution token, and `reversible: false` engages the manual floor unless the attested operator policy explicitly permits the class-scoped exception in §5.2. (Amended APRV-317, pending sign-off.)
|
|
293
|
+
|
|
294
|
+
The irreversibility floor normally resolves to `manual`: an action declared `reversible: false` MUST be raised to `manual` after class resolution unless the attested operator policy explicitly allows the class to retain its nonmanual autonomy through §5.2's `allow_irreversible` rule. When allowed, the resolved autonomy, supervision mode, rates, approvers and limits remain in force. Manual actions still require a decision for each action; supervised-live actions wait only when selected by the existing live sampler; supervised and supervised-retro actions proceed and remain eligible for retrospective review; autonomous actions proceed without approval or retrospective sampling. Existing sampling failure behavior, budgets, attestation, execution binding and other independent floors remain unchanged. A Telegram prompt occurs only on a path that actually requests approval through that channel. A policy-authorized execution MUST NOT be represented as a human grant. (Amended APRV-317, pending sign-off.)
|
|
291
295
|
|
|
292
|
-
The
|
|
296
|
+
The decision trace MUST distinguish an applied manual floor, explicit policy permission, an already-manual resolution, and a human-only resolution, and identify the governing rule group. A floored resolution carries no supervision mode or rate. **The floor stops at `manual` and never changes `human-only`.** An override cannot enable agent execution of a human-only class or relax protected-policy controls. Existing policies behave unchanged because the new key defaults to false. Adding it is a policy edit, inoperative until human re-attestation; requests and grants pinned to an older policy remain subject to the existing policy-drift checks and must be re-requested where those checks refuse them. Policy amendment summaries MUST show changes to irreversible resolution, including an allow-only edit whose ordinary resolution stays unchanged. (Amended APRV-317, pending sign-off.)
|
|
293
297
|
|
|
294
|
-
**The floor is a floor, not a proof.** `reversible` is self-reported by the party
|
|
298
|
+
**The floor is a floor, not a proof.** `reversible` is self-reported by the party under oversight, so §11.1 invariant 4 governs it: the field may raise scrutiny and never lower it. A truthful `false` raises a nonmanual class unless the operator has explicitly accepted irreversible execution at that class's declared autonomy. A false claim of `true`, or an omitted claim, fails to raise scrutiny and proves nothing about reversibility. The permission comes only from the attested policy, never from action metadata, an envelope field, or prose guidance. An author who requires every action in a class to be approved writes `manual`; no declaration can loosen that rule. Reconciliation obligations remain in force whichever truthful declaration selects their shape (§5.2). (Amended APRV-317, pending sign-off.)
|
|
295
299
|
|
|
296
300
|
For `record.*` classes, grant means adoption: the action proposes a write to a system of record (a task stage, a note category, a pipeline state), and approval commits it. The "adapter" is whatever write path owns the record; it MUST hold proposed writes in a staged state invisible to, or visibly provisional in, the record proper until granted. `record.*` actions are typically reversible; policies gate them for cognitive ownership rather than consequence, and both rationales are first-class (see §11).
|
|
297
301
|
|
|
@@ -308,6 +312,7 @@ For `record.*` classes, grant means adoption: the action proposes a write to a s
|
|
|
308
312
|
```
|
|
309
313
|
|
|
310
314
|
- `hash` = SHA-256 over the canonical serialization of the record with `prev` included; `prev` = previous record's hash. `approval log verify` MUST detect any mutation or truncation. Optionally, the log directory is a git repo and the daemon commits per event with its own identity, giving signed, distributed tamper evidence for free (the [TaskChampion operation log](https://github.com/GothenburgBitFactory/taskchampion) and [Automerge](https://automerge.org) both converged on op-logs for related reasons; see also Ink & Switch's [local-first task framework](https://www.inkandswitch.com/patchwork/notebook/tasks-01/)).
|
|
315
|
+
- **Verified subscription.** A log subscription MUST treat filesystem notifications only as prompts to read. Before emitting a batch it MUST verify the complete chain from genesis through the observed head, and MUST emit no record from a batch that fails verification or its retained cursor binding. Corrupt, torn, unreadable and cursor-mismatched input MUST remain distinct terminal failures using the existing integrity, torn-tail and I/O refusal categories. The resume cursor is exclusive. A retained `(seq, hash)` binds the resume point to the previously consumed prefix; sequence alone is a weaker bootstrap and cannot detect a fully recomputed replacement prefix on its first read. After accepting that first read, the subscription MUST retain the cursor hash even when no newer record is available. Output MUST respect consumer backpressure, and cancellation MUST release subscription resources. Reconnect delivery is at least once: a consumer that performs an external effect MUST persist its cursor after that effect and provide its own idempotency or transaction if it requires exactly-once effects. This read-only stream neither repairs the log nor grants authority. (Amended APRV-322, pending sign-off.)
|
|
311
316
|
- **Event types (v0.1):** `task.registered`, `route.proposed`, `route.accepted`, `approval.requested`, `approval.granted`, `approval.rejected`, `approval.expired`, `approval.revoked`, `approval.withdrawn`, `execution.started`, `execution.completed`, `execution.failed`, `execution.indeterminate`, `execution.reconciled`, `budget.exceeded`, `policy.updated`, `envelope.drift`, `audit.sampled`, `audit.reviewed`, `payload.pruned`, `gate.opened`, `gate.closed`, `gate.bypassed`, `gate.organ.attested`, `log.checkpoint`.
|
|
312
317
|
- **Enum versioning.** `payload.pruned` is the first addition to the draft v0.1 set of sixteen types, `approval.withdrawn` the second, and `execution.indeterminate` with `execution.reconciled` the third and fourth. Readers of a v0.1 log may encounter any of them, and a verifier that treated the draft set as closed MUST be updated to accept all four. `execution.indeterminate` names the task and the action key like every other execution event, carries the executing actor, and its payload carries a `reason` drawn from a closed set (`act-threw` at v0.1) and, when present, an `exit_code` of `null`. `execution.reconciled` names the same task and key, carries a `human:` actor, and its payload carries `indeterminate_seq`, a `resolution` of `executed` or `not-executed`, a non-empty `note`, and `attested_by_human: true`. That the named `indeterminate_seq` is an unreconciled `execution.indeterminate` for this key is a rule the gate enforces (§6.3); a schema sees one record and can only constrain the shape. (Amended APRV-120.) `payload.pruned` is written by the daemon alone, carries a `system:` actor, and names the pruned payload by its SHA-256 (§5.2 `payload_retention`). `approval.withdrawn` names the task and the action key like every other approval event, carries the requester's own actor (`agent:` or `human:`, never `system:`), and its payload carries `action_key` and a `reason` drawn from `timeout`, `cancelled`, `superseded`, with an optional `note`. That the actor equals the actor of the matching `approval.requested` is a rule the gate enforces (§6.3); a schema sees one record and can only rule out the actor kind. (Amended APRV-106.) `gate.opened`, `gate.closed` and `gate.bypassed` record the open window of §5.2. The first two carry a `human:` actor and never any other; `gate.opened` carries `expires_at`, `duration`, `reason` and `scope` (`hook` at v0.1), and `gate.closed` carries `opened_seq`. `gate.bypassed` carries the harness's own actor (`agent:` or `human:`, never `system:`), and its payload carries `opened_seq`, `tool`, `summary`, `classes` and `payload_hash`, with `session_id`, `tool_use_id` and `cwd` optional; the summary is bounded and the full bytes are named by their hash, so the record's surface is the one the gated path already has. That the named `opened_seq` is an unexpired, unclosed `gate.opened` is a rule the hook enforces; a schema sees one record. (Amended APRV-214.) `gate.organ.attested` records a human's sign-off on the exact bytes of one of the gate's organs (§5.2). It carries a `human:` actor and never any other, for the reason `gate.opened` does, and its payload carries exactly two required fields: `organ_path`, the file's repository-relative and `/`-separated path, and `sha256`, the digest of its bytes. Both are computed by the runtime and neither is a caller's parameter. The path is the whole relative path rather than a basename, unlike `policy.updated`'s `policy_path`, because several organs live in several directories and a digest attested for one of them is not evidence about another. That the named path is a `policy.core` surface, and is neither the policy file nor inside the approval home, is a rule the runtime enforces; a schema sees one record and can only constrain the shape. It is a distinct type and not a `policy.updated` variant so that no reader of the policy attestation can mistake one for the other: an implementation that reuses `policy.updated` here MUST make every policy-attestation reader ignore organ records, and the reference runtime discharges the requirement by construction instead. (APRV-272.) `log.checkpoint` records a human's signature over a chain head (§9). It carries a `human:` actor and never any other, and its payload carries exactly `seq` and `hash` (the head that was signed), `alg` (the signature scheme, `ed25519` at v0.1), `key_sha256` (the fingerprint of the signing key), and `signature`. That the signed `seq` is below the record's own, and that the log carries the named hash at that seq, are rules the verifier enforces; a schema sees one record and can only constrain the shape. The fingerprint names a key rather than carrying one, because a record carrying its own public key would invite a reader to verify the signature against it, which any forger could satisfy: the authority is the policy's declared list. (Amended APRV-220.)
|
|
313
318
|
- **The provider reference.** An `execution.completed` MAY carry `payload.provider_ref`, an object of exactly two non-empty strings and no others: `adapter`, the name of the adapter that executed the action, and `id`, the identifier the provider's own record files the effect under (an AgentMail `message_id`, a queue's receipt handle). Both are written by the runtime at the write boundary. `adapter` is the registered adapter's name, which the runtime already holds; `id` is lifted from what the adapter returned for that call and passes the same redaction sweep as every other string it returned (§11.1 invariant 3). The field exists to be a join key. `approval coverage` (§10.1) reads a provider's own record of what it did and asks whether this log ever saw each effect, and with no reference the strongest available answer is a record of a matching class inside the effect's window, so one gated send covers an ungated one of the same class beside it. With a reference, a record that names an effect by id is evidence about that exact effect, and an effect the provider recorded that no record names is a gap no window can close. It authorizes nothing: no grant, budget, verdict, or refusal reads it back, so §11.1 invariant 4 is untouched by a value that came in part from the far side. It is bounded in length and restricted to printable characters, so that an identifier cannot become a place to put a message body. It is OPTIONAL and additive: an execution whose adapter names no reference records none, every record written before the field existed still validates and still verifies, and a reader treats absence as the pre-amendment behaviour. An id the redaction sweep touched is omitted rather than recorded, because a redacted identifier matches nothing and would read like one that does. (Amended APRV-251.)
|
|
@@ -338,6 +343,8 @@ Every displayed field is one of two kinds and MUST be visibly distinguished: **c
|
|
|
338
343
|
|
|
339
344
|
```
|
|
340
345
|
approval init # scaffold APPROVAL.md, .approval/, gitignore
|
|
346
|
+
approval quickstart # human-only solo setup, review exact policy
|
|
347
|
+
# bytes, type understood, then attest
|
|
341
348
|
approval instructions # full agent-facing usage guide (also in --help)
|
|
342
349
|
approval register <task-file> # validate envelope, append task.registered
|
|
343
350
|
approval request <task> [--action <key>] # -> approval.requested (manual classes;
|
|
@@ -349,7 +356,11 @@ approval token <action-key> # report execution-token status (the token it
|
|
|
349
356
|
# is printed once, by `grant`; only its hash is logged)
|
|
350
357
|
approval run -- <cmd…> # gate arbitrary commands: mints token, runs, logs
|
|
351
358
|
approval queue [--json] # pending requests
|
|
352
|
-
approval log verify | tail | export
|
|
359
|
+
approval log verify | tail | export | follow
|
|
360
|
+
approval log follow --from <seq> [--cursor-hash <64hex>] --json
|
|
361
|
+
# foreground JSON Lines; exclusive from defaults to 0
|
|
362
|
+
# exit 0: SIGINT/SIGTERM or downstream pipe closure;
|
|
363
|
+
# existing exits 1/2/3/4: integrity/usage/torn/I-O
|
|
353
364
|
approval log sync # fast-forward the committed log under the append
|
|
354
365
|
# lock, with a snapshot and a chain reconcile
|
|
355
366
|
approval log advance [--pr] # commit the log's new records onto a records
|
|
@@ -373,6 +384,8 @@ approval hook claude-code # gate an agent harness: reads a PreToolUse
|
|
|
373
384
|
approval hook cursor # gate a local Cursor Agent: native
|
|
374
385
|
# preToolUse JSON in, {permission}
|
|
375
386
|
# allow/deny out (never "ask")
|
|
387
|
+
approval hook codex # experimental native Codex hook: direct apply_patch
|
|
388
|
+
# only; Bash is explicitly refused (§10.6)
|
|
376
389
|
approval hook classify -- <cmd…> # what the classifier makes of a command
|
|
377
390
|
approval reindex | render
|
|
378
391
|
approval daemon run # the §10.2 watch loop, in the foreground
|
|
@@ -414,6 +427,10 @@ A request MAY declare self-delivery, which mints the sealed delivery address reg
|
|
|
414
427
|
|
|
415
428
|
**Neither verb appends an event.** The log records decisions with real-world consequence. Moving the file the log is stored in is housekeeping on the container rather than a decision about the world, and an event for it would be the log narrating its own filesystem. Implementations MUST NOT append a record for either operation. (Amended APRV-125.)
|
|
416
429
|
|
|
430
|
+
**Interactive quickstart.** `approval quickstart` is a human-only, terminal-only authoring and attestation ceremony for a fresh instance. It MUST refuse machine-readable mode and non-terminal input before writing, MUST NOT overwrite or attest an existing policy, and MUST validate and display the complete generated policy before asking the operator to type `understood`. The attestation append MUST compare the file bytes it reads with the digest of those displayed bytes and refuse a mismatch without appending. Setup or health-preflight failure before attestation leaves the new policy unattested; successful append is the attestation boundary. The generated autonomous default does not remove protected-class floors, fail-closed policy handling, or unreadable-command refusals. The command MAY explicitly resolve only the new instance's declared environment sources for its diagnostic child, as described in §5.2; it MUST discard ambient approval credential variables before supplying the freshly resolved values and the entered identity. It MUST preserve an explicitly selected service endpoint in setup and diagnostics, and print an explicit `approval env --dir` activation command for the target directory. It does not activate the caller's shell, install a service, or change another instance. (Amended APRV-309, pending sign-off.)
|
|
431
|
+
|
|
432
|
+
**Constrained Codex preparation.** The opt-in `approval codex` command family prepares a separately installed execution boundary; package installation MUST NOT activate hooks, managed configuration, services, principals, credentials or policy. `prepare` writes only a fresh inert review bundle containing an explicit instance manifest, configuration and launcher templates, installation instructions and file digests. `setup --check` verifies the bundle's exact contents and reports its inert status; it does not install it. `doctor --strict` distinguishes manifest validity and filesystem custody from demonstrated enforcement, rejects unknown runtime versions or platforms and unsafe or ambiguous paths, and MUST NOT execute a manifest-selected binary before establishing its trusted custody. The manifest pins disjoint workspace, primary gate and installation roots, exact executable and interpreter paths and invocation, and distinct non-root Codex, broker and runner principals. Trusted runtime and configuration paths must be outside agent write custody. Missing components, unchecked confinement, or an unverified installation MUST remain not-ready, and `start` or `serve` MUST refuse rather than fall back to the ordinary broad MCP server or unconstrained execution. This command family is excluded from broad MCP publication. Neither a passing bundle check nor files present on disk proves that the session is bound to the gate; that claim requires verified execution-boundary behavior for the installed version and all exposed capabilities. (Amended APRV-325.1, pending sign-off.)
|
|
433
|
+
|
|
417
434
|
### 10.2 Daemon
|
|
418
435
|
|
|
419
436
|
`approvald` watches the backlog folder and the log: validates new/changed envelopes, applies policy, dispatches channel notifications, expires TTLs, samples supervised actions for audit, re-renders projections, and (optionally) polls upstream sources. Loop safety: three consecutive `execution.failed` events for one task escalate to `manual` regardless of policy.
|
|
@@ -472,7 +489,7 @@ The ceremony owns its own git preconditions. It fetches the remote, refuses unle
|
|
|
472
489
|
|
|
473
490
|
### 10.4 Adapters and hard enforcement
|
|
474
491
|
|
|
475
|
-
Adapters (e.g. `adapter-email`, `adapter-agentmail`, `adapter-gcal`) hold the actual credentials in an encrypted vault and MUST require a valid, unexpired, single-use execution token bound to
|
|
492
|
+
Adapters (e.g. `adapter-email`, `adapter-agentmail`, `adapter-gcal`) hold the actual credentials in an encrypted vault and MUST execute through the runtime's verified policy and execution boundary. A manual action, including an irreversible action whose policy does not explicitly permit otherwise and a selected supervised-live action, MUST require a valid, unexpired, single-use execution token bound to its `idempotency_key`. A policy-authorized autonomous, supervised-retro (including the supervised alias), or non-selected supervised-live action does not require a token; the attested policy, registered payload binding, budgets, execution recording and remaining floors still govern it. No implementation may fabricate a human grant for that path. The boundary is the adapter contract, and bypassing the CLI confers no authority over credentials held exclusively behind it. (Same architectural intuition as mission-control's vault + "agents cannot modify security settings.") An adapter MAY additionally implement an optional `observe(window)` that reports what its provider recorded happening, which is read-only, spends no token and is called outside any grant window, so that §10.1's coverage report can witness an adapter-backed class without any authority being exercised. (Amended APRV-245.) (Amended APRV-317, pending sign-off.)
|
|
476
493
|
|
|
477
494
|
An execution token is bound to the request, its `idempotency_key`, AND its `payload_hash`. Adapters and `approval run` MUST recompute the hash of the payload they are about to execute and MUST refuse, with a distinct machine-readable reason (`payload-mismatch`), when it differs from the hash the grant recorded. A grant therefore approves specific bytes. Changing the payload after grant requires a new request.
|
|
478
495
|
|
|
@@ -485,7 +502,7 @@ An executor that spawns a child MUST construct that child's environment rather t
|
|
|
485
502
|
|
|
486
503
|
A provider whose keys carry per-permission booleans lets that boundary be made hard on the provider's own side, and `adapter-agentmail` is where this specification says how. AgentMail issues keys on which `draft_create`, `draft_update` and `draft_read` are separate permissions from `draft_send` and `message_send`, so a deployment issues two: the agent holds a key carrying the composing permissions and no send permission at all, and the vault holds a key carrying the send permissions, read only inside the verified-token window the adapter contract opens. Without that split, any key sitting in the agent's environment sends without consulting this runtime, which is the case §11 already states plainly as undefended: an agent with direct credential access outside adapters. The provider publishes no pre-send webhook, so no other place exists where a send in flight could be held for a human, and the permission boolean is the whole of the enforcement; an implementation MUST NOT claim a stronger property than the key it was handed supports. The second thing this adapter settles is how a grant binds a REMOTE MUTABLE object, which follows from §6.2 rather than adding to it. A draft lives on the provider's side and the agent may rewrite it after a human has read it, so an approval of its identifier would be an approval of whatever it holds at send time. The grant therefore binds the bytes fetched at request time: the payload carries the inbox and draft identifiers alongside the recipients, the subject and the text as they stood when the request was opened, and the payload hash covers those bytes like any other. Before it sends, the adapter re-fetches the object, compares those same fields, and refuses with its own machine-readable reason (`agentmail-draft-drifted`) when any of them differ. That reason is distinct from `payload-mismatch` because the fact and the repair are both different: the bytes the caller presented are the approved ones and the far side moved underneath them, so the grant stands and a fresh request is owed to the human rather than to the file. The refusal names WHICH fields differ and never what they now hold, since a refusal is written to a log that a human who approved none of the new text will read, and quoting it there would publish unapproved content through the refusal path. (Amended APRV-224.) That comparison happens twice, and the first of the two runs BEFORE the token is consumed, as the adapter's pre-token check. A drift found there refuses with nothing appended and the grant intact, so the same token sends once the approved text is restored; a drift found by the second comparison, which runs inside the consumed-token window immediately before the send, is an `execution.started` followed by an `execution.failed`, which is the honest record of a window that was open when the far side moved. Running the comparison only in the second position spends a human's single-use grant to discover a send that never happened (found on a live inbox, 2026-09-06), and a refusal that costs another tap teaches operators to stop checking, which is the one lesson this design cannot afford to teach. The pre-spend read is performed with the SENDING key from the vault, resolved in the pre-token credential window described below, and never with a key the calling agent holds in its own environment. The reason is invariant 4 of §11.1: the comparison decides whether the agent's own edit counts as drift, so a comparison whose input that agent supplies is scrutiny the party under oversight controls. Nothing new is opened to make this possible, since the vault already answers before the spend for an adapter's declared credentials, inside the presented-phase grant minted only when the caller's token matches the digest the grant recorded. A pre-token check is offered only bytes the log binds to the action (the grant's `payload_hash` on the manual path, the registered declaration's off it); anything else is `payload-mismatch`, which the runtime refuses in its own words with nothing appended and the token still live. (Amended APRV-276.)
|
|
487
504
|
|
|
488
|
-
The reference runtime gives that boundary a definite shape. An adapter implements one method, `act`, over two things: the payload the grant
|
|
505
|
+
The reference runtime gives that boundary a definite shape. An adapter implements one method, `act`, over two things: the payload bound by the grant or registered declaration, and a credential provider scoped to that call. The runtime's adapter contract owns everything around it, in a fixed order: recompute the payload hash, verify the applicable policy authority and consume a token where required, append `execution.started`, call `act`, append `execution.completed` or `execution.failed`. An adapter cannot skip a step, because it never holds the sequence. Credentials reach `act` only inside the runtime-authorized execution window, and the provider refuses every request made after `act` returns, so an adapter that keeps its reference gets a refusal rather than a secret. Whatever the adapter reports back (its own failure code, message, and detail) is scanned for the credential values it was handed and redacted before the runtime records or returns any of it, which makes §11.1's third invariant a mechanism at this boundary rather than a convention. Adapters written elsewhere are held to the same sequence by the conformance suite that ships with the contract, in the way §10.3's channels are held to their display rules. (Amended APRV-67.) References to token consumption and a grant window in this section describe paths that require a grant. The no-token path follows the same payload, recording, idempotency, redaction and credential-window closure rules; it carries no human-decision claim. The existing environment-source-map fallback remains strictly token-backed and MUST NOT open for a policy-authorized no-token action, which uses only credential configuration already established in the process environment by the operator. (Amended APRV-317, pending sign-off.)
|
|
489
506
|
|
|
490
507
|
An adapter declares the credential names it cannot act without, and those names MUST resolve before the token is consumed: a credential the runtime cannot reach refuses `credential-unavailable` with nothing appended and the grant intact, so a configuration fault costs no authority and the same token executes once the credential is reachable. The side effect's own ordering is unchanged, and deliberately so: the token is still consumed and `execution.started` still appended before `act` is called, because that ordering is what makes a crash between the two an ambiguity the log can show (§10.4 above, on custody). Inside that consumed-token window, and only there, an implementation MAY resolve the vault passphrase the policy names from the instance's own environment source map (§5.2) when the ambient environment does not carry it, which narrows that file's "nothing loads it implicitly" rule to exactly one caller: the authority is the token, a human approved this specific action, and the resolved value goes to the vault and reaches no argv, no log, no message, and no other verb. Every other verb still sees only the environment a human established. (Amended APRV-169 and APRV-168.) An adapter MAY also declare a pre-token check, which the runtime calls in that same pre-spend position, after the declared credentials resolve and before the token is consumed. It is read-only, it is offered only bytes the log binds to the action, and its refusal is reported under one stable code (`adapter-precheck-refused`) carrying the adapter's own reason, with nothing appended and the same token spendable once the named condition is repaired. It generalises what `credential-unavailable` established: a condition that makes the side effect impossible, and that the runtime can establish without attempting it, MUST NOT cost a human's single-use grant to discover. It never stands in for a check inside the window, since the far side can move between the two and the check that binds the bytes actually sent is the later one, and a check that raises rather than answers is treated as a refusal, because a check that could not be performed is not a check that passed. (Amended APRV-276.)
|
|
491
508
|
|
|
@@ -495,15 +512,27 @@ Indeterminate is a custody state, not a result. The consumption is burned: the t
|
|
|
495
512
|
|
|
496
513
|
It resolves only through an explicit reconciliation, invoked by a person and never by the daemon, fed by evidence from the relying party rather than from this runtime's own log. Recovery here is never evidence that the provider did not execute. The resolution is appended as its own `execution.reconciled` record NAMING the indeterminate one, which is never rewritten, so the original observation survives its resolution and an auditor sees both the doubt and its answer. Resolving as `not-executed` is recorded distinctly from resolving as `executed`, and re-opens the possibility of the EFFECT rather than of the action: an `idempotency_key` is the global identity of one side effect (§6.2) and a used one stays used, so a still-wanted effect is declared as a fresh action and requested again. (Amended APRV-120.)
|
|
497
514
|
|
|
498
|
-
The reference vault is the storage half of the same sentence. Named credentials live in one file beside the log, `.approval/vault.enc`, holding a JSON map of name to credential encrypted with AES-256-GCM under a key derived by scrypt from an operator passphrase. The passphrase is read from the environment variable the policy names in `vault.passphrase_env` (§5.2), so the policy an agent may read carries a variable name and the value lives outside the repository. The file records its own format version and KDF parameters, so a future scheme is a migration rather than a reinterpretation of old bytes, and every write re-encrypts the whole map under a fresh nonce and lands atomically. The write path is human-only (`approval vault set | list | remove`, identity resolved exactly as `policy attest` resolves it), `list` reports names and never values, and there is deliberately no verb that prints a credential: the value's only sanctioned journey is from the vault into an adapter's `act`, through a credential provider the contract above scopes to the
|
|
515
|
+
The reference vault is the storage half of the same sentence. Named credentials live in one file beside the log, `.approval/vault.enc`, holding a JSON map of name to credential encrypted with AES-256-GCM under a key derived by scrypt from an operator passphrase. The passphrase is read from the environment variable the policy names in `vault.passphrase_env` (§5.2), so the policy an agent may read carries a variable name and the value lives outside the repository. The file records its own format version and KDF parameters, so a future scheme is a migration rather than a reinterpretation of old bytes, and every write re-encrypts the whole map under a fresh nonce and lands atomically. The write path is human-only (`approval vault set | list | remove`, identity resolved exactly as `policy attest` resolves it), `list` reports names and never values, and there is deliberately no verb that prints a credential: the value's only sanctioned journey is from the vault into an adapter's `act`, through a credential provider the contract above scopes to the runtime-authorized execution window described above. What the vault defends is credentials at rest and casual reads by an agent with file access, since the ciphertext hides the names as well as the values. What it does not defend, stated as plainly as §11 states the rest, is a compromised host or an agent that can read the passphrase variable; such an agent decrypts the file directly and needs no adapter. The vault raises the cost of a credential leak from reading a file to owning the session, and claims nothing beyond that. The reference runtime's interactive writer for adapter credentials is `approval setup adapter <name>`, driven by the credential manifest the adapter declares: it asks for each named value, validates every answer with the adapter's own rules, stores the set in the vault, and offers to verify the result against the service without sending anything. (Amended APRV-68. The `approval setup adapter <name>` sentence is APRV-78.) (Amended APRV-317, pending sign-off.)
|
|
499
516
|
|
|
500
517
|
For `manual` actions, channels MUST present the full payload or a faithful rendering of it, clearly delineated from any agent-written summary, before collecting a decision.
|
|
501
518
|
|
|
519
|
+
**Adapter eligibility and supervised-live intake.** Before resolving credentials or running a provider-backed precheck, an adapter MUST verify the principal, registered action and exact payload, current applicable authority, execution custody and idempotency. A successful eligibility preview consumes no token and appends no execution start; budget refusals retain the ordinary budget audit record and compare-and-append race handling. For a direct no-token supervised-live action with no prior approval cycle, the adapter MUST use the existing request intake and daemon draw, deriving request metadata from verified registration and supplying the complete bound payload for retention and display. A selected or unavailable draw follows the ordinary pending human-approval path; it does not count as an unselected verdict. A prior cycle, including a pending, rejected or expired request, MUST NOT be rerolled to avoid that decision. An unselected verdict MUST remain bound to the same attested policy digest through eligibility and final start. After precheck, the execution path MUST revalidate authority, payload, policy binding and concurrency before recording a start or acting. Source-map credential fallback remains restricted to verified token-backed execution. (Amended APRV-317, pending sign-off.)
|
|
520
|
+
|
|
502
521
|
### 10.5 MCP wrapper (optional)
|
|
503
522
|
|
|
504
523
|
A thin MCP server exposing the same verbs as tools for clients where MCP is more ergonomic than shelling out. It shares the CLI's code paths.
|
|
505
524
|
|
|
506
|
-
What shipped is `approval mcp serve`: a foreground server speaking MCP over stdio, running as one `agent:<id>` the operator fixes when they start it. The tool surface is the agent-facing half of the verb registry, one tool per verb (`register`, `request`, `wait`, `run`, `queue`, `status`, `log_verify`, and the rest). Human-only verbs are absent by design, which is §11's argument applied to a transport: the agent is the untrusted policy and the human is the trusted overseer, an MCP client is an agent's harness, so publishing `grant` on it would hand the untrusted policy the overseer's pen.
|
|
525
|
+
What shipped is `approval mcp serve`: a foreground server speaking MCP over stdio, running as one `agent:<id>` the operator fixes when they start it. The tool surface is the agent-facing half of the verb registry, one tool per verb (`register`, `request`, `wait`, `run`, `queue`, `status`, `log_verify`, and the rest). Human-only verbs are absent by design, which is §11's argument applied to a transport: the agent is the untrusted policy and the human is the trusted overseer, an MCP client is an agent's harness, so publishing `grant` on it would hand the untrusted policy the overseer's pen. Five agent-facing verbs are withheld as well, for transport reasons rather than authority reasons: `consume`, which is internal plumbing that `run` wraps; `hook claude-code` / `hook cursor` / `hook codex`, which each read their event from the stdin this transport already owns; and `log follow`, whose unbounded foreground stream cannot be represented as one finite tool result and would occupy the serialized call queue indefinitely. A conformance reader building the tool list from this section takes it to be the registry filtered by `human_only`, minus those five. Tool descriptions and input schemas are derived from the same registry `approval instructions --schemas` prints, with `--as` deleted from every published schema, so a caller cannot name an identity; the server's own identity is appended last to every argv, so it wins even where one arrives by another route. A tool call builds an argv and invokes the function the CLI dispatches to, so a refusal is the CLI's refusal, returned as a tool result carrying `{"error":{"code","message"}}` rather than as a protocol error. The [MCP tasks extension](https://modelcontextprotocol.io) and elicitation MAY be mapped onto `awaiting` when client support stabilizes; that remains post-v1, and until then `wait` blocks and answers. A2A's `input-required` maps cleanly for agent-to-agent deployments. Under `token_delivery: sealed` (§10.4) the `wait` tool MAY return the raw execution token in its result, which retires the earlier reading that a token an agent could fetch would be a grant an agent could give itself. Fetching a minted token is not minting one: the token exists only because a human granted it, binds to the payload bytes, and is single-use, and the tool returns it only to the process that opened the request and holds the private key. What it replaces is a human copying that same value out of a terminal. `grant` remains absent from this surface, and that is the line that matters — the untrusted policy still cannot hold the overseer's pen. (Amended APRV-88, APRV-103, APRV-105.) (Amended APRV-322, pending sign-off.)
|
|
526
|
+
|
|
527
|
+
### 10.6 Experimental native Codex hook adapter (Amended APRV-313)
|
|
528
|
+
|
|
529
|
+
The opt-in `approval hook codex` adapter synchronously reads native `PreToolUse` and `PostToolUse` events under the exact `Bash|apply_patch` matcher. Direct `apply_patch` may enter the verified gate, policy, budgets, approval wait and grant-consumption paths. Current native `Bash` pre-execution input MUST be explicitly denied before gate-open, self-delivery, grant reuse, registration or execution start, because session cwd does not establish the effective per-call execution directory. Bash MUST remain matched so a shell-dispatched patch cannot bypass that refusal. Enabling Bash requires named-version native evidence that the effective directory is bound to the exact input and stable session and tool-call identity; a caller assertion or native approval does not substitute. See `docs/codex-hook.md` for the observed-version evidence. The default gate wait is nine minutes and the example native hook timeout is ten minutes.
|
|
530
|
+
|
|
531
|
+
A direct patch approval MUST bind the complete raw patch, exact tool identity, stable identifiers and execution directory. Every Add, Update, Delete and Move source and destination MUST pass the existing scope and protected-path checks; an unreadable or ambiguous patch is refused. An explicit allow MUST echo the exact approved command through `updatedInput`; a denial MUST never rewrite input. Codex sandbox permissions remain independent. Codex hook configuration (`.codex/hooks.json`, `.codex/hooks/` and `.codex/config.toml`) is a gate organ under §5.2, and inert examples MUST remain outside those live paths.
|
|
532
|
+
|
|
533
|
+
Codex has its own `codex` harness provenance. A post-execution report MUST correlate with a verified delegated start for that Codex call and use `reported_by: post-tool-use`. Only a structured outcome verified against a named Codex version may close the matched start. The runtime MUST NOT infer an outcome from an event name or output text, and tool output MUST NOT enter the approval log. Unknown, interrupted or unfinished outcomes remain open with a diagnostic.
|
|
534
|
+
|
|
535
|
+
Operator documentation MUST distinguish direct-patch coverage from Bash refusal, native crash, timeout and malformed-response fail-open behavior, input rewriting by another hook that invalidates the approved binding, and unverified desktop and phone behavior. Doctor MUST distinguish configuration presence from trusted or demonstrated operation. This experimental adapter provides no universal enforcement or strict Claude parity. (Amended APRV-313.)
|
|
507
536
|
|
|
508
537
|
## 11. Security and control model
|
|
509
538
|
|
|
@@ -538,7 +567,7 @@ The following hold across every surface of the runtime. They are implicit accept
|
|
|
538
567
|
1. **Enforcement paths read only verified records.** Gate decisions are computed from log state that has passed chain verification, never from unverified or partially read input (`tests/state.test.ts`). *Scope note:* the attestation-required list of §5.2 (request intake, grant recording, token minting) enumerates operations rather than modules, and the harness hooks of §14 (`approval hook claude-code`, `approval hook cursor`) are one of those surfaces: a hook verdict that lets a command run with no human in the loop is a gate decision, so it MUST verify policy attestation and loop-escalation against the verified log before it allows, and MUST fail closed when it cannot reach the log (`tests/cli-hook.test.ts`). (Amended APRV-139.) *Scope note:* reading only verified records means a surface can hold a record the view it has just read does not carry, and a harness adapter is where that bites: it appends its requests, re-reads, and after a log synchronisation or a daemon restart the verified view can be behind its own writes. The absence of a record for an action key the surface itself appended, or found pending in its own earlier verified read, MUST NOT be read as a terminal state, because a log is append-only and a request that existed does not stop existing. The response is to keep waiting inside the bound the surface already has and to report that the view lags; an implementation MUST NOT read unverified bytes to resolve it, and MUST NOT allow on a record the verified chain does not carry (`tests/cli-hook.test.ts`). (Amended APRV-294.)
|
|
539
568
|
2. **Gate-typed events never accept caller timestamps.** `ts` on gate-typed events is assigned by the runtime at the write boundary; a caller-supplied value is refused (`tests/clock.test.ts`).
|
|
540
569
|
3. **Raw secrets never appear in the log.** What appears is a hash, or ciphertext sealed to a recipient key the log does not hold. Execution tokens and binding material are logged as hashes; under sealed token delivery (§10.4) `approval.granted` additionally carries the token encrypted to the requester's ephemeral public key, and the private half of that key lives outside the log and outside version control. A raw-token scan over written logs finds nothing under either delivery mode, and the sealed field does not open without the private key (`tests/token.test.ts`, `tests/binding.test.ts`, `tests/sealed-delivery.test.ts`). The rewording relaxes nothing: an attacker with a copy of the log — the one artifact this system deliberately makes durable, copyable and auditable — still gains no ability to execute. (Amended APRV-105.)
|
|
541
|
-
4. **Self-reported fields never reduce scrutiny.** Values authored by the party under oversight may raise scrutiny and never lower it (`tests/ratchet.test.ts`). *Scope note:* a self-reported field that CONTROLS something is therefore bounded twice — by what it can move and by what it can be trusted to prove. `reversible` is the worked example: it may raise a class to `manual` through the §7 floor, it may choose between two reconciliation obligations that both exist (§5.2), and it may never remove an obligation, lower an autonomy, or stand as evidence that an action can be undone. A control keyed to such a field is a floor, not a proof, and implementations MUST say so where they document it (`tests/autonomy-split.test.ts`). (Amended APRV-127.) *Scope note:* the open window of §5.2 lowers the scrutiny every hook-gated call receives, and it does not offend this invariant, because its author is the human, who is not the party under oversight. The agent's own fields are unchanged by it: an agent cannot open one (the ceremony classifies `policy.core`, needs a terminal, and has no flag that answers for a person), and inside one nothing an agent reports moves what stays denied (`tests/gate-window.test.ts`, `tests/cli-gate-window.test.ts`). (Amended APRV-214.)
|
|
570
|
+
4. **Self-reported fields never reduce scrutiny.** Values authored by the party under oversight may raise scrutiny and never lower it (`tests/ratchet.test.ts`). *Scope note:* a self-reported field that CONTROLS something is therefore bounded twice — by what it can move and by what it can be trusted to prove. `reversible` is the worked example: it may raise a class to `manual` through the §7 floor, it may choose between two reconciliation obligations that both exist (§5.2), and it may never remove an obligation, lower an autonomy, or stand as evidence that an action can be undone. A control keyed to such a field is a floor, not a proof, and implementations MUST say so where they document it (`tests/autonomy-split.test.ts`). (Amended APRV-127.) *Scope note:* the open window of §5.2 lowers the scrutiny every hook-gated call receives, and it does not offend this invariant, because its author is the human, who is not the party under oversight. The agent's own fields are unchanged by it: an agent cannot open one (the ceremony classifies `policy.core`, needs a terminal, and has no flag that answers for a person), and inside one nothing an agent reports moves what stays denied (`tests/gate-window.test.ts`, `tests/cli-gate-window.test.ts`). (Amended APRV-214.) *Scope note:* §7's explicit irreversible-action permission comes from the attested operator policy and never from a self-reported action field. An agent cannot create this policy permission through action metadata; a false reversibility declaration remains untrusted and proves nothing. The existing attestation and human-only controls still bind. (Amended APRV-317, pending sign-off.)
|
|
542
571
|
5. **Every check-then-append passes through compare-and-append.** No path reads a decision-relevant log state and appends on it without the atomic head check that makes the pair safe under concurrency (`tests/concurrency.test.ts`, `tests/log.test.ts`).
|
|
543
572
|
6. **Refusals are machine-readable and distinct, and every code union is pinned by a test.** Each refusal path returns its own stable code, and the unions are frozen public API (`tests/gate.test.ts`, `tests/token.test.ts`, `tests/execute.test.ts`, `tests/log.test.ts`, for the open window's verbs `tests/gate-window.test.ts`, for the log-anchoring check `tests/log-anchor.test.ts`, for the human-signed checkpoint check `tests/log-checkpoint.test.ts`, and for the audit verbs `tests/audit.test.ts`). (Amended APRV-214. The anchoring union is APRV-219, the checkpoint union APRV-220.) (Amended APRV-237.)
|
|
544
573
|
7. **Configuration is never loaded implicitly from the working tree.** No verb reads a working-directory file into its own environment; the environment a gate operation runs under is established by the human who launched the process (`tests/cli-env.test.ts`). (Amended APRV-73.)
|
|
@@ -730,7 +759,7 @@ Milestones sized for agent-driven development (each = one reviewable task):
|
|
|
730
759
|
- **M5** Daemon: watch, TTL, sampling, loop-escalation.
|
|
731
760
|
- **M6** Backlog.md round-trip + AGENTS.md import.
|
|
732
761
|
- **M7** First adapter (email) + vault; end-to-end demo: agent drafts chaser → Telegram ping → approve from phone → sent → log verifies. A second adapter (AgentMail) serves the same class over an HTTPS API and is where §10.4's two-key enforcement model and its binding of a remote mutable draft are exercised (APRV-224).
|
|
733
|
-
- **M8** MCP wrapper (§10.5) and the agent-harness hooks, `approval hook claude-code` and `approval hook
|
|
762
|
+
- **M8** MCP wrapper (§10.5) and the agent-harness hooks, `approval hook claude-code`, `approval hook cursor`, and opt-in `approval hook codex` (§10.6): a harness PreToolUse adapter that classifies the command a harness is about to run and resolves it against the policy. Ordinary Edit/Write calls resolve `files.write.workspace` through the same policy and accounting path as shell actions. A manual class waits on a recorded decision; every authorized execution, including supervised and autonomous executions, records `execution.started` before proceeding, with a matching outcome when the harness reports a verified result. A human-open gate window follows §5.2 instead: it records `gate.bypassed` and preserves human-only refusal. (Amended APRV-304.) Both surfaces expose the same gate to a client that is not a shell. Applications built on the Claude Agent SDK are reachable through the same Claude Code hook surface: the SDK's hook callbacks receive that PreToolUse event and return that permission decision, so a documented shim spawning `approval hook claude-code` gates them with no new surface (`docs/agent-sdk-hook.md`). The §13 Rust fast-path is this hook's post-v1 latency accelerator, not a prerequisite. Post-v1: TickTick/GCal sinks, inbound capture adapters. (Amended APRV-103. Amended APRV-242.)
|
|
734
763
|
|
|
735
764
|
## 15. References
|
|
736
765
|
|