approval-md 0.2.0 → 0.3.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 +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/docs/cli-reference.md
CHANGED
|
@@ -390,6 +390,13 @@ did left conflict markers inside the log mid-ceremony.
|
|
|
390
390
|
So the ritual became deterministic code, on the `policy amend` precedent: when a
|
|
391
391
|
hand-ritual proves dangerous, it becomes a verb the gate can read.
|
|
392
392
|
|
|
393
|
+
**You rarely type it any more (APRV-346).** `approval up`'s preflight calls this
|
|
394
|
+
function itself whenever the working log is a byte-for-byte extension of the
|
|
395
|
+
committed one, which is the state every records advance leaves behind. What is
|
|
396
|
+
left for a hand-run `approval log sync` is the fork: two chains that share a
|
|
397
|
+
prefix and then carry different records at the same `seq`. See
|
|
398
|
+
[up](#up) for what the preflight checks before it delegates.
|
|
399
|
+
|
|
393
400
|
Everything runs inside ONE hold of the append lockfile. The lock is normally
|
|
394
401
|
taken per append; here it spans the whole operation, because an append landing
|
|
395
402
|
between the snapshot and the restore is exactly the interleaving that forks a
|
|
@@ -485,6 +492,12 @@ range they cover, and pushed to a short-lived records branch that exists for
|
|
|
485
492
|
exactly that commit. Main is protected here, so the commit reaches it through a
|
|
486
493
|
pull request; `--pr` opens that pull request through the ordinary `gh` path.
|
|
487
494
|
|
|
495
|
+
This verb carries the class `log.advance` for everyone who runs it, in a
|
|
496
|
+
session, in an orchestrator, or at a human terminal. The daemon's cadence
|
|
497
|
+
advance is the one that may carry `log.advance.daemon` instead, and only from
|
|
498
|
+
inside the daemon process: see the `--advance` paragraph under `daemon run`
|
|
499
|
+
(APRV-382).
|
|
500
|
+
|
|
488
501
|
`--co-author "Name <email>"` adds one validated `Co-authored-by` trailer to the
|
|
489
502
|
generated records commit and to the pull request body. When the day's pull
|
|
490
503
|
request already exists, the verb preserves its body and adds the trailer once.
|
|
@@ -712,7 +725,21 @@ refusal {"ok":false,"error":{"code":"...","message":"..."}} on stderr
|
|
|
712
725
|
|
|
713
726
|
`path` is the file that was hashed; the logged payload carries its basename only,
|
|
714
727
|
so an exported log leaks no home directory. The event's payload is
|
|
715
|
-
`{"policy_path":"APPROVAL.md","sha256":"<64 hex>"}`.
|
|
728
|
+
`{"policy_path":"APPROVAL.md","sha256":"<64 hex>","payload_hash":"<64 hex>"}`.
|
|
729
|
+
|
|
730
|
+
**`payload_hash`: the attested bytes, recoverable (APRV-356).** The verb also
|
|
731
|
+
writes the attested text to the payload store beside the log and binds that
|
|
732
|
+
file's hash on the record, so the policy IN FORCE can be produced rather than
|
|
733
|
+
only named. A digest cannot produce the file it names, and the privileged-gesture
|
|
734
|
+
rule (`policy.core` gestures from a channel, SPEC.md §10.3) has to be decided
|
|
735
|
+
against the policy in force rather than against the one being proposed. A reader
|
|
736
|
+
re-hashes the recovered text against `sha256` from the verified log, so the store
|
|
737
|
+
is checked rather than trusted, and a store that cannot be written REFUSES the
|
|
738
|
+
attestation: nothing is appended, because a record claiming a binding whose bytes
|
|
739
|
+
are absent is a worse artifact than no record. Records written before this
|
|
740
|
+
existed still validate and verify; a reader treats the absent field as the
|
|
741
|
+
pre-amendment state, where the in-force bytes are unrecoverable and the
|
|
742
|
+
fail-closed fallback applies.
|
|
716
743
|
|
|
717
744
|
### `--organ <path>`: the gate's organs (APRV-272)
|
|
718
745
|
|
|
@@ -769,6 +796,92 @@ current bytes carry no attestation. It never moves doctor's exit code: an
|
|
|
769
796
|
unattested organ breaks nothing on this machine, and the enforcement for one is
|
|
770
797
|
the guard in CI.
|
|
771
798
|
|
|
799
|
+
### `--path <path>`: signing off a protected file (APRV-338)
|
|
800
|
+
|
|
801
|
+
SPEC.md's amendment-provenance rule says text that reached a protected file
|
|
802
|
+
without a grant carries `(Amended APRV-n, pending sign-off.)` and holds no more
|
|
803
|
+
authority than a proposal until a human ratifies it. Nothing recorded the
|
|
804
|
+
ratification: no event, no verb, and no way for CI or doctor to tell a ratified
|
|
805
|
+
amendment from a pending one. This flag is that record.
|
|
806
|
+
|
|
807
|
+
```
|
|
808
|
+
approval policy attest --path SPEC.md --as human:carter
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
One path per call, repository-relative (an absolute path under `--dir` is
|
|
812
|
+
accepted and recorded relative). The runtime hashes the bytes on disk; there is
|
|
813
|
+
no flag for the digest. The record is a **`gate.path.signed_off`** event:
|
|
814
|
+
|
|
815
|
+
```
|
|
816
|
+
{"event":"gate.path.signed_off","actor":"human:<id>",
|
|
817
|
+
"payload":{"path":"SPEC.md","sha256":"<64 hex>"}}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
**Which paths.** Exactly those whose edits classify `policy.edit` or a
|
|
821
|
+
`policy.edit.*` sub-class: the built-in prose set (`CLAUDE.md`, `AGENTS.md`,
|
|
822
|
+
`.npmrc`, `.github/workflows/`) plus everything the live policy's
|
|
823
|
+
`protected_paths` widens to, including a path routed to a sub-class such as
|
|
824
|
+
`policy.edit.design`. The policy is loaded to answer that, and a policy that
|
|
825
|
+
does not load contributes nothing, which narrows what may be signed rather than
|
|
826
|
+
widening it.
|
|
827
|
+
|
|
828
|
+
Three refusals are specific to this flag, all exit 2:
|
|
829
|
+
|
|
830
|
+
```
|
|
831
|
+
path-is-policy the policy file: use `approval policy attest` with no flag
|
|
832
|
+
path-is-core a gate organ (use --organ), the approval home, or the log
|
|
833
|
+
directory, which no verb ratifies
|
|
834
|
+
path-not-protected an ordinary file, or a path that is not repository-relative
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
`--organ` and `--path` together are a usage error, as are `--policy` and
|
|
838
|
+
`--path`: each pair names two different claims and guessing which one was meant
|
|
839
|
+
is how the wrong record gets written. `--json` adds `signed_path`:
|
|
840
|
+
|
|
841
|
+
```
|
|
842
|
+
success {"ok":true,"seq":7,"sha256":"<64 hex>","path":"/abs/SPEC.md",
|
|
843
|
+
"signed_path":"SPEC.md"}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**Weaker than a grant, and read last.** A grant binds the exact hunk a human saw
|
|
847
|
+
in a prompt; a sign-off stands for the whole file. So the protected-path guard
|
|
848
|
+
asks about a sign-off only after its search for a grant covering the change has
|
|
849
|
+
come up empty, and the finding it prints says so in words. A change that a grant
|
|
850
|
+
does cover still passes on the grant and still names it. Signing off is for the
|
|
851
|
+
case the pending-sign-off suffix was invented for: text a human has read at that
|
|
852
|
+
commit and agrees with, for which no grant was ever taken.
|
|
853
|
+
|
|
854
|
+
**Signing off bytes that are on a branch.** The digest the guard checks is the
|
|
855
|
+
file's blob at the commit under review, and that is usually a pull request's
|
|
856
|
+
head rather than anything on disk in the primary checkout. `--dir` and `--log`
|
|
857
|
+
are separate flags for exactly this: `--dir` is the checkout whose bytes are
|
|
858
|
+
hashed and which the recorded path is relative to, `--log` is the log the record
|
|
859
|
+
is appended to. So a human ratifying an open pull request reads the change,
|
|
860
|
+
puts a checkout at that commit somewhere (a `git worktree`, or the branch
|
|
861
|
+
checked out in a scratch clone), and runs:
|
|
862
|
+
|
|
863
|
+
```
|
|
864
|
+
approval policy attest --path SPEC.md \
|
|
865
|
+
--dir /path/to/checkout-at-that-commit \
|
|
866
|
+
--log /path/to/primary/.approval/log/events.jsonl \
|
|
867
|
+
--as human:<id>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
The record lands in the primary checkout's log, where a log advance carries it
|
|
871
|
+
to a records branch, and the guard then finds it for that path at that digest.
|
|
872
|
+
The verb never writes a log inside the worktree it hashed. If the digest it
|
|
873
|
+
prints is not the one the guard's failure named, the checkout is at the wrong
|
|
874
|
+
commit or the file has been edited since: the two must agree exactly, and a
|
|
875
|
+
mismatch is a sign-off on bytes nobody reviewed.
|
|
876
|
+
|
|
877
|
+
The verb classifies `policy.core` when `--path` is present (without it the verb
|
|
878
|
+
attests the gate's own configuration and stays pass-through), so under this
|
|
879
|
+
repository's policy the harness hook denies it to an agent with
|
|
880
|
+
`hook-class-human-only` before the verb's own `actor-not-human` refusal is
|
|
881
|
+
reached. `approval doctor`'s `pending-sign-off` row lists the protected files
|
|
882
|
+
that still carry the marker with no record over their current bytes; like
|
|
883
|
+
`gate-organs` it never moves doctor's exit code.
|
|
884
|
+
|
|
772
885
|
## policy amend
|
|
773
886
|
|
|
774
887
|
**Progress, on stderr.** The verb re-verifies the whole chain and recovers the
|
|
@@ -832,6 +945,16 @@ resolution still prints in the semantic diff below for the human who attests it.
|
|
|
832
945
|
Until APRV-296 every declared class had to be pinned, in both directions, and a
|
|
833
946
|
one-line TTL amendment on 2026-09-07 took three runs to land because of it.
|
|
834
947
|
|
|
948
|
+
The one check every declared class still faces is REACHABILITY: a class nothing
|
|
949
|
+
can emit is a line that will never fire, and the ceremony refuses it rather than
|
|
950
|
+
leaving an operator believing in it for a year. Three ways to be reachable: the
|
|
951
|
+
command classifier's fixed table, a `protected_paths` entry routing a path
|
|
952
|
+
family to a `policy.edit` sub-class, and `RUNTIME_CLASSES` in
|
|
953
|
+
`src/core/command-class.ts`, which names the classes a runtime cycle asks the
|
|
954
|
+
gate for directly and no command spells (`log.advance.daemon` is the first,
|
|
955
|
+
APRV-382). A new class of that third kind needs its line there in the SAME build
|
|
956
|
+
the ceremony runs, or the amendment refuses `policy-suite-failed`.
|
|
957
|
+
|
|
835
958
|
**And then the whole dogfood suite, still before the attestation.** The pin check
|
|
836
959
|
is a subset of `tests/dogfood.test.ts`, and the seq 23351 ceremony passed the pins
|
|
837
960
|
and went red on CI over a dogfood test about the values block. So `--commit` also
|
|
@@ -912,15 +1035,62 @@ base carries it, and a pins edit somebody else landed is not reverted by this
|
|
|
912
1035
|
ceremony. The pin deltas print in the semantic diff beside the class deltas, ride
|
|
913
1036
|
in the commit subject, and appear in `--json` as `pins`.
|
|
914
1037
|
|
|
915
|
-
It refuses outside a git repository, and refuses
|
|
916
|
-
|
|
917
|
-
staged edit would make "this commit is
|
|
1038
|
+
It refuses outside a git repository (`commit-preconditions`), and refuses
|
|
1039
|
+
`staged-unrelated` when the index holds staged changes to anything beyond those
|
|
1040
|
+
three: a commit that swept in an unrelated staged edit would make "this commit is
|
|
1041
|
+
the amendment" false. It refuses `dirty-tree` when one of those three is staged
|
|
1042
|
+
in one state and modified again in the working tree, because the commit is
|
|
1043
|
+
assembled from the WORKING TREE and would otherwise carry bytes the operator's
|
|
1044
|
+
`git diff --cached` never showed. An unrelated *unstaged* path is deliberately
|
|
1045
|
+
not a refusal: the scratch index lays exactly the ceremony's paths over the
|
|
1046
|
+
remote's tree, so nothing else can reach the commit, and refusing over one would
|
|
1047
|
+
stop the ceremony in the checkout it is written for, where the daemon's envelope
|
|
1048
|
+
write-backs leave task files modified as a matter of course. On the branch flow
|
|
918
1049
|
it also refuses when there is no `origin` remote, and when a `--branch` name
|
|
919
1050
|
already exists. Every one of those refusals happens BEFORE the attestation, so a
|
|
920
1051
|
refused `--commit` never leaves an attested policy without its commit. The same
|
|
921
1052
|
holds for the fetch, the two base checks, the policy suite and the dogfood suite
|
|
922
1053
|
above.
|
|
923
1054
|
|
|
1055
|
+
**`--pr`: the ceremony finishes its own job (APRV-341).** `--pr` is `--commit`
|
|
1056
|
+
plus "and publish it": it forces the BRANCH flow whatever the protection probe
|
|
1057
|
+
answered, so the amendment is committed on `origin/<default branch>` in a scratch
|
|
1058
|
+
index, pushed to `policy-amend-<seq>`, carried by a pull request, and armed with
|
|
1059
|
+
`gh pr merge <branch> --merge --auto`. The merge queue picks the strategy from
|
|
1060
|
+
there. `--pr --direct` and `--pr --no-publish` are usage errors: each pair asks
|
|
1061
|
+
for opposite ceremonies.
|
|
1062
|
+
|
|
1063
|
+
The pull request is OPENED or UPDATED. `gh pr list --head <branch> --state open`
|
|
1064
|
+
is asked first, and when one is already standing its title and body are edited
|
|
1065
|
+
rather than a second being opened, so a re-run of a ceremony that stopped
|
|
1066
|
+
half-way finishes rather than failing at `gh pr create`. A `gh` that cannot
|
|
1067
|
+
answer the question falls through to `create`, which is the path that was there
|
|
1068
|
+
before.
|
|
1069
|
+
|
|
1070
|
+
**The verb never switches branches.** On 2026-09-16 an agent-written runbook for
|
|
1071
|
+
the primary checkout ended with `git checkout main` after the amend commit, which
|
|
1072
|
+
rewound `APPROVAL.md` and `events.jsonl` under a live appender; the hook then
|
|
1073
|
+
appended 204 records on the stale chain and the log forked. `--pr` exists so that
|
|
1074
|
+
runbook does not: the commit is assembled with `git read-tree` into a scratch
|
|
1075
|
+
index and pushed by sha, the checkout ends the verb on the branch it started on,
|
|
1076
|
+
with the same HEAD, the same index and the same working tree, and the only file
|
|
1077
|
+
that moved is the log, which gained the attestation. A test compares all four
|
|
1078
|
+
before and after.
|
|
1079
|
+
|
|
1080
|
+
Without `--pr` (and on a box with no `gh`) the printed runbook does the same
|
|
1081
|
+
thing by hand, and it does not switch branches either (APRV-360): `approval log
|
|
1082
|
+
sync`, `git add`, `git commit`, `git push origin HEAD:refs/heads/policy-amend-<seq>`,
|
|
1083
|
+
`gh pr create --head`, `gh pr merge --auto --merge`. A fixture test pins the six
|
|
1084
|
+
commands and asserts that no printed line contains a `git checkout`, because a
|
|
1085
|
+
fallback nobody checks is where the branch switch came back.
|
|
1086
|
+
|
|
1087
|
+
The form printed before APRV-360 opened with `git checkout -b policy-amend-<seq>
|
|
1088
|
+
origin/main`. On 2026-09-18 the primary's main was fourteen commits behind, the
|
|
1089
|
+
switch refused rather than overwrite `QUEUE.md`, the working log and six
|
|
1090
|
+
payloads, and the ceremony stalled with an edited, attested, unpublished policy.
|
|
1091
|
+
`approval log sync` is the first line now for that reason: it brings the checkout
|
|
1092
|
+
current, and it refuses `log-diverged` rather than fast-forwarding over a fork.
|
|
1093
|
+
|
|
924
1094
|
`--commit` also pushes, on both flows. When there is no `origin` to push to, the
|
|
925
1095
|
direct flow reports the push as still to run rather than listing it among the
|
|
926
1096
|
commands it ran. `--no-publish` stops the ceremony at the commit: nothing is
|
|
@@ -962,9 +1132,9 @@ beneath it.
|
|
|
962
1132
|
sha256 8acbd01cda98
|
|
963
1133
|
|
|
964
1134
|
Committed
|
|
965
|
-
✓ committed the policy and the
|
|
1135
|
+
✓ committed the policy, the log and the attested policy text together:
|
|
966
1136
|
|
|
967
|
-
git add APPROVAL.md .approval/log/events.jsonl
|
|
1137
|
+
git add APPROVAL.md .approval/log/events.jsonl .approval/payloads/8acbd01cda98….json
|
|
968
1138
|
git commit -m "Policy: amend APPROVAL.md: 1 class resolution(s) (attested seq 2)"
|
|
969
1139
|
|
|
970
1140
|
Publishing
|
|
@@ -1057,10 +1227,16 @@ is the human rendering only.
|
|
|
1057
1227
|
prints the SEMANTIC diff, computed by the real engine on both versions;
|
|
1058
1228
|
4. runs the load advisory;
|
|
1059
1229
|
5. asks for confirmation (skipped by `--yes` and `--dry-run`);
|
|
1060
|
-
6. attests: one `policy.updated` event, identical to `approval policy attest
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1230
|
+
6. attests: one `policy.updated` event, identical to `approval policy attest`,
|
|
1231
|
+
which since APRV-356 also stores the attested bytes in the payload store and
|
|
1232
|
+
binds their hash on the record;
|
|
1233
|
+
7. prints, or with `--commit` runs, the git ceremony — `git add <policy> <log>
|
|
1234
|
+
<attested bytes>` (plus the pins when they moved), a `git commit` citing the
|
|
1235
|
+
attestation seq, and the push (and, on the branch flow, the branch and the
|
|
1236
|
+
pull request). The store file rides in the same commit because a committed
|
|
1237
|
+
log carrying a binding whose bytes were never committed leaves the policy in
|
|
1238
|
+
force unrecoverable for everyone reading that copy, the CI protected-path
|
|
1239
|
+
guard included;
|
|
1064
1240
|
8. publishes, unless `--no-publish`: a push the remote refuses is answered by
|
|
1065
1241
|
the branch, push, pull request and auto-merge above, each reported as it
|
|
1066
1242
|
lands, and a step that fails drops to the runbook from there.
|
|
@@ -1117,7 +1293,7 @@ attestation may still proceed.
|
|
|
1117
1293
|
"publishing":null|{"attempted":true,"complete":true,
|
|
1118
1294
|
"via":"direct"|"branch"|"recovery"|"none",
|
|
1119
1295
|
"branch":null|"policy-amend-2","pushed":true,
|
|
1120
|
-
"prUrl":null|"https://...",
|
|
1296
|
+
"prUrl":null|"https://...","prUpdated":false,
|
|
1121
1297
|
"autoMerge":"armed"|"refused"|"not-attempted",
|
|
1122
1298
|
"steps":[{"command":"git push origin main","ok":false}],
|
|
1123
1299
|
"stoppedAt":null|"git push -u origin policy-amend-2",
|
|
@@ -1149,10 +1325,19 @@ the message.
|
|
|
1149
1325
|
- `io` — the policy file or the log could not be read or written.
|
|
1150
1326
|
- `load-failed` — `--require-load` and the policy does not load. Nothing was
|
|
1151
1327
|
appended.
|
|
1152
|
-
- `commit-preconditions` — `--commit` outside a git repository, with
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1328
|
+
- `commit-preconditions` — `--commit` outside a git repository, with the policy
|
|
1329
|
+
and the log in different repositories, or (branch flow) with no origin remote
|
|
1330
|
+
or a `--branch` name already taken. Checked before the attestation; nothing
|
|
1331
|
+
was appended.
|
|
1332
|
+
- `staged-unrelated` — the index carries staged changes beyond the policy, the
|
|
1333
|
+
log and the pins (APRV-341). Its own code because its repair is its own: one
|
|
1334
|
+
`git restore --staged <path>`. Checked before the attestation; nothing was
|
|
1335
|
+
appended.
|
|
1336
|
+
- `dirty-tree` — one of those three is staged in one state and modified again in
|
|
1337
|
+
the working tree (APRV-341). The commit is assembled from the working tree, so
|
|
1338
|
+
it would carry bytes `git diff --cached` does not show. An unrelated *unstaged*
|
|
1339
|
+
path is not this refusal and never was. Checked before the attestation;
|
|
1340
|
+
nothing was appended.
|
|
1156
1341
|
- `fetch-failed` / `base-policy-diverged` / `base-log-diverged` — the remote the
|
|
1157
1342
|
amendment would be based on could not be fetched, carries a policy this edit
|
|
1158
1343
|
was not written against, or carries a log this working log does not contain.
|
|
@@ -1181,6 +1366,114 @@ the message.
|
|
|
1181
1366
|
- `log-unreadable` / `log-torn-tail` / `log-corrupt` — nothing is amended from a
|
|
1182
1367
|
log that does not verify.
|
|
1183
1368
|
|
|
1369
|
+
## policy apply
|
|
1370
|
+
|
|
1371
|
+
**The hand-paste this replaces (APRV-343).** Agents may not write `APPROVAL.md`:
|
|
1372
|
+
it is `policy.core`, and this project's policy holds that class human-only. So a
|
|
1373
|
+
policy change an agent proposes travels as a document under `docs/proposals/`
|
|
1374
|
+
that quotes each current line byte for byte beside its replacement, and the
|
|
1375
|
+
human pastes. Two things go wrong with a paste, and both have:
|
|
1376
|
+
|
|
1377
|
+
- **a whole-file copy reverts what it did not know about.** The prepared file was
|
|
1378
|
+
written against the policy as it stood when the proposal was drafted, so
|
|
1379
|
+
anything landed in between is silently undone. That is the failure
|
|
1380
|
+
`base-policy-diverged` catches for the commit, one step too late to help;
|
|
1381
|
+
- **a paste carries its wrapper.** One paste of a proposal page took the page's
|
|
1382
|
+
own fence with it, which hid a block from the loader (APRV-273).
|
|
1383
|
+
|
|
1384
|
+
`approval policy apply <proposal.md>` answers both by construction. It writes no
|
|
1385
|
+
byte that is not anchored to a byte it proved present in the live file, and it
|
|
1386
|
+
reads fences by their backtick run, so a four-backtick wrapper around a
|
|
1387
|
+
three-backtick block is the wrapper it is rather than part of the content.
|
|
1388
|
+
|
|
1389
|
+
### The proposal format
|
|
1390
|
+
|
|
1391
|
+
A proposal is ordinary markdown. Anywhere in it, a fenced block **with a
|
|
1392
|
+
declared language** whose immediately preceding non-blank line is a label is a
|
|
1393
|
+
member of a pair:
|
|
1394
|
+
|
|
1395
|
+
| label | means |
|
|
1396
|
+
|---|---|
|
|
1397
|
+
| `Current:` | the bytes as they stand in the live policy |
|
|
1398
|
+
| `Replace with:` | what they become |
|
|
1399
|
+
| `Supersedes:` | optional: an earlier section's RESULT, to match instead |
|
|
1400
|
+
|
|
1401
|
+
A fence with no info string is skipped, which is APRV-273's hazard turned into a
|
|
1402
|
+
rule. A block whose label names no role is skipped too, so a proposal page can
|
|
1403
|
+
carry a `bash` block of commands to run afterwards without the applier mistaking
|
|
1404
|
+
it for policy text. Pairs apply in document order.
|
|
1405
|
+
|
|
1406
|
+
**Supersession is declared, never inferred from position.** A later section that
|
|
1407
|
+
rewrites a line an earlier section already rewrote quotes the earlier section's
|
|
1408
|
+
*result* under a `Supersedes:` label; the applier looks for that text when the
|
|
1409
|
+
section's own `Current` block is no longer in the file, which is exactly the
|
|
1410
|
+
state the earlier section left behind. Position could not carry this: two
|
|
1411
|
+
sections that touch one line are not in general in the order the file needs, and
|
|
1412
|
+
a rule inferred from order is a rule nobody can read off the page. A pair whose
|
|
1413
|
+
`Current` **and** `Supersedes` blocks both occur in the file is
|
|
1414
|
+
`proposal-ambiguous`: two spellings of one line is a question about which the
|
|
1415
|
+
file means, and no verb here will pick.
|
|
1416
|
+
|
|
1417
|
+
**Whole-file replacement is not accepted**, and that is the decision rather than
|
|
1418
|
+
an omission. The verb's whole value is that every byte it writes is anchored to
|
|
1419
|
+
a byte it proved present, which is what makes a stale proposal a refusal instead
|
|
1420
|
+
of a silent revert. A whole-file blob has no anchor. `approval policy amend`
|
|
1421
|
+
over a hand-edited file is already the supported way to replace the file
|
|
1422
|
+
deliberately.
|
|
1423
|
+
|
|
1424
|
+
**The values block is treated exactly as the policy block is**, by knowing
|
|
1425
|
+
nothing about either. The applier is a byte-level replacement over the whole
|
|
1426
|
+
file: it does not parse the policy, does not locate blocks, and does not care
|
|
1427
|
+
which fence a pair lands in. The values block is inert (SPEC §11.1 invariant
|
|
1428
|
+
10), so applying one changes no verdict, and the attestation the amendment
|
|
1429
|
+
appends covers the whole file's bytes either way (SPEC §5.2, §5.3).
|
|
1430
|
+
|
|
1431
|
+
`docs/proposals/README.md` is the contract as a page for proposal authors, and
|
|
1432
|
+
`docs/proposals/approval-md-2026-09.md` is a worked example of every part of it.
|
|
1433
|
+
|
|
1434
|
+
### What it does, in order
|
|
1435
|
+
|
|
1436
|
+
1. Refuses an agent identity (`apply-agent-actor`) before reading anything.
|
|
1437
|
+
2. Parses the proposal into ordered pairs.
|
|
1438
|
+
3. Resolves every pair against an **in-memory** copy of the policy. A proposal
|
|
1439
|
+
whose third pair is stale writes nothing at all, so "a stale proposal cannot
|
|
1440
|
+
half-apply" is a property of the code rather than of the order somebody wrote
|
|
1441
|
+
the sections in.
|
|
1442
|
+
4. Prints the replacements, each as its matched block and its replacement.
|
|
1443
|
+
5. Asks for confirmation (`--yes` skips it, `--dry-run` stops here).
|
|
1444
|
+
6. Writes the policy, then runs `approval policy amend --pr` in this process —
|
|
1445
|
+
which asks its OWN question about the semantic diff, because "are these the
|
|
1446
|
+
bytes" and "is this the policy" are different questions.
|
|
1447
|
+
|
|
1448
|
+
**It publishes by default (APRV-360).** The amendment runs with `--pr`, so the
|
|
1449
|
+
branch is created on the remote by refspec, the pull request is opened, the
|
|
1450
|
+
merge is armed, and the checkout ends where it started. Before this the amend
|
|
1451
|
+
ran with no flag: the policy was written and attested, and the operator was
|
|
1452
|
+
handed a procedure that began with a branch switch. On 2026-09-18 the primary
|
|
1453
|
+
refused that switch and the ceremony stalled with an edited, attested,
|
|
1454
|
+
unpublished policy, which is the one state in which every gated operation on the
|
|
1455
|
+
box refuses. `--no-publish` stops at the commit and is passed through to the
|
|
1456
|
+
amend; `--pr` is accepted and does nothing, because runbooks already say it, and
|
|
1457
|
+
`--pr` with `--no-publish` is a usage error.
|
|
1458
|
+
|
|
1459
|
+
`--no-amend` writes and stops, and says loudly that the policy is now edited and
|
|
1460
|
+
unattested. A run where every pair resolves and no byte moves is a success and a
|
|
1461
|
+
no-op: the proposal has already been applied.
|
|
1462
|
+
|
|
1463
|
+
**Refusal codes** (`error.code` with `--json`; frozen public API): `usage`,
|
|
1464
|
+
`io`, `apply-agent-actor`, `proposal-empty`, `proposal-malformed` (a `Current`
|
|
1465
|
+
with no `Replace with`, or the reverse), `proposal-stale` (a quoted current text
|
|
1466
|
+
is not in the file), `proposal-ambiguous` (it occurs more than once, or both it
|
|
1467
|
+
and its superseded text occur). Every one of them writes nothing.
|
|
1468
|
+
|
|
1469
|
+
Two outcomes are deliberately not in that union. Answering no at the
|
|
1470
|
+
confirmation is exit 0 with `aborted:` on stdout, exactly as `policy amend`
|
|
1471
|
+
answers it: nothing failed, and an error object at exit 0 would be a
|
|
1472
|
+
contradiction the caller has to resolve. An amendment that refuses has already
|
|
1473
|
+
printed its own code from its own frozen union, so this verb adds a sentence
|
|
1474
|
+
naming the state that leaves behind — the replacements written, the policy
|
|
1475
|
+
unattested — and returns the amendment's exit code unchanged.
|
|
1476
|
+
|
|
1184
1477
|
## register
|
|
1185
1478
|
|
|
1186
1479
|
The task file is read only. Nothing is rewritten, so unknown frontmatter keys
|
|
@@ -1798,7 +2091,7 @@ refusal {"ok":false,"error":{"code":"...","message":"...","detail"?:"...",
|
|
|
1798
2091
|
## sandbox
|
|
1799
2092
|
|
|
1800
2093
|
```
|
|
1801
|
-
approval sandbox [--allow-loopback] [--log <path>] -- <cmd> [args…]
|
|
2094
|
+
approval sandbox [--allow-loopback] [--read-jail] [--log <path>] -- <cmd> [args…]
|
|
1802
2095
|
```
|
|
1803
2096
|
|
|
1804
2097
|
Runs a command with outbound network denied by the operating system. It appends
|
|
@@ -1836,6 +2129,15 @@ is a sandbox somebody turns off.
|
|
|
1836
2129
|
server. It is a real widening: a port is a port, and anything listening on one is
|
|
1837
2130
|
reachable from inside.
|
|
1838
2131
|
|
|
2132
|
+
**`--read-jail`** (APRV-347) goes the other way and makes the room smaller: file
|
|
2133
|
+
reads become deny-default, with the gate root, the scratch roots and a fixed
|
|
2134
|
+
runtime set opened by `subpath`. It is already on, without the flag, whenever
|
|
2135
|
+
the policy declares a `read_scope` block, which is the spelling an operator
|
|
2136
|
+
commits and attests; the flag is for trying it on one command first. There is no
|
|
2137
|
+
flag that turns the jail OFF where a policy asked for it, because a flag an agent
|
|
2138
|
+
can pass must only ever narrow what it can do. See
|
|
2139
|
+
[docs/sandboxed-exec.md](./sandboxed-exec.md) for the profile and its limits.
|
|
2140
|
+
|
|
1839
2141
|
**Exit 127** means the command was NOT run: this machine has no working sandbox
|
|
1840
2142
|
primitive, or the command is not on `PATH`. Unlike `approval run`, this verb
|
|
1841
2143
|
fails closed on both: it makes one promise and has nothing else to offer.
|
|
@@ -2530,6 +2832,23 @@ The checks, at length:
|
|
|
2530
2832
|
log sync` for a diverged log, `approval up` otherwise — never a `git` command:
|
|
2531
2833
|
a repair line telling an operator to reset a branch would be doctor making the
|
|
2532
2834
|
decision this project keeps human.
|
|
2835
|
+
- **attested-policy-on-main** — whether the policy the log vouches for is the
|
|
2836
|
+
policy `origin/<branch>` carries (APRV-342). `attestation` above asks whether
|
|
2837
|
+
the LOCAL file is attested; between a `policy amend` and its pull request
|
|
2838
|
+
merging that answer is yes while a fresh checkout of main carries the old
|
|
2839
|
+
policy with no attestation covering it, so every gate operation there refuses
|
|
2840
|
+
`policy-not-attested`. Nothing said so until this row: on 2026-09-16 `approval
|
|
2841
|
+
up` ran in exactly that state and reported "already at the remote tip". PASS
|
|
2842
|
+
when the attested hash equals the SHA-256 of `APPROVAL.md` at the remote tip.
|
|
2843
|
+
FAIL with `attested at seq N, not yet on main`, naming
|
|
2844
|
+
`policy-amend-<seq>` when this checkout has already seen that branch on the
|
|
2845
|
+
remote, and fixing with `approval policy amend --pr` — which opens the pull
|
|
2846
|
+
request or updates the open one, so the same command is right either way.
|
|
2847
|
+
SKIP with no attestation, outside a git checkout, and where there is no
|
|
2848
|
+
remote-tracking ref. **It fetches nothing**, for the reason
|
|
2849
|
+
`main-behind-origin` fetches nothing, and it looks for the amend branch among
|
|
2850
|
+
the remote-tracking refs rather than asking GitHub. `approval up`'s preflight
|
|
2851
|
+
prints the same sentence on stderr and never refuses on it.
|
|
2533
2852
|
- **harness-version-unverified** — whether the harness binary hosting the
|
|
2534
2853
|
PreToolUse hook changed since the log last saw a record from it (APRV-227).
|
|
2535
2854
|
The only row that asks anything about a program outside this repository, and
|
|
@@ -2573,7 +2892,14 @@ The checks, at length:
|
|
|
2573
2892
|
words SPEC.md §5.3 fixes: a file with no block is an operator who has declared
|
|
2574
2893
|
no values, which is a state and not a fault. The only FAIL is a block that is
|
|
2575
2894
|
present and unreadable, and its fix names the code rather than proposing a
|
|
2576
|
-
repair, because what the block should say is the human's to write.
|
|
2895
|
+
repair, because what the block should say is the human's to write. The one
|
|
2896
|
+
exception is the block APRV-336 replaced (`version: 1` with a `wants` list):
|
|
2897
|
+
that is a correct document of the wrong vintage rather than a broken one, so
|
|
2898
|
+
the row's detail carries the loader's migration message and its fix names the
|
|
2899
|
+
three edits (fold `wants:` into `like:`, rename `responds:` to
|
|
2900
|
+
`communication:`, quote `version: "0.2"`) plus the re-attestation that the
|
|
2901
|
+
whole-file digest requires. The pass detail lists what the block declares,
|
|
2902
|
+
which are `love`, `like`, `dislike` and `communication` and nothing else.
|
|
2577
2903
|
- **checkpoint** — how this log stands against its own human-signed checkpoints
|
|
2578
2904
|
(APRV-257), running the same check as `approval log verify --checkpoints`, so
|
|
2579
2905
|
two implementations of "does this log's own signature contradict it" cannot
|
|
@@ -2622,6 +2948,57 @@ The checks, at length:
|
|
|
2622
2948
|
doctor does not interpret or merge the TOML hook tables. Malformed JSON
|
|
2623
2949
|
FAILS; a different valid Codex hook profile SKIPS as undetermined rather than
|
|
2624
2950
|
being called broken.
|
|
2951
|
+
- **autonomy-alias** — which rules of this policy still write the deprecated
|
|
2952
|
+
bare `supervised` (APRV-335). The spelling parses as `supervised-retro` and
|
|
2953
|
+
every gate enforces it as one, so the row is never a FAIL and never moves the
|
|
2954
|
+
exit code: a red line over a spelling would be doctor going red over prose.
|
|
2955
|
+
The two PASS shapes are the whole row. Where some rule uses it, the detail
|
|
2956
|
+
names each one, in the order the loader's own notes name them, and the `fix`
|
|
2957
|
+
is `approval policy amend`, which owns the edit and the re-attestation that
|
|
2958
|
+
edit costs. Where none does, the detail says so plainly, which is the answer
|
|
2959
|
+
an operator wants before a future schema version drops the alias. A policy
|
|
2960
|
+
that did not load is a SKIP: it names no level at all, and its own failure is
|
|
2961
|
+
reported by the attestation row and by `approval policy check`.
|
|
2962
|
+
- **pending-sign-off** — which protected files carry SPEC.md's
|
|
2963
|
+
`(Amended APRV-n, pending sign-off.)` marker with no `gate.path.signed_off`
|
|
2964
|
+
record over their current bytes (APRV-338). Informational and never a FAIL,
|
|
2965
|
+
for the two reasons `gate-organs` is: nothing on this machine is broken by
|
|
2966
|
+
unratified prose, and the enforcement that does bite is the CI-side
|
|
2967
|
+
protected-path guard. What the row buys is that the debt is visible at the
|
|
2968
|
+
terminal rather than discovered when a pull request fails. It reads the
|
|
2969
|
+
enumerated directories (`.`, `.github/workflows/`, `docs/`, `design/`) plus
|
|
2970
|
+
every path the policy's `protected_paths` names, keeps only what classifies
|
|
2971
|
+
`policy.edit` or a `policy.edit.*` sub-class, and reports a file whose current
|
|
2972
|
+
bytes ARE signed off as ratified rather than pending. The `fix` is
|
|
2973
|
+
`approval policy attest --path <p> --as human:<id>`, to be run after reading
|
|
2974
|
+
the file; a marker whose text was later granted through the gate should lose
|
|
2975
|
+
the suffix instead.
|
|
2976
|
+
- **sender-mapping** — which approvers a channel whose senders the policy maps
|
|
2977
|
+
can still recognize (APRV-324). A SKIP where no approver declares a `senders`
|
|
2978
|
+
block, which is every installation before the key existed: decisions are
|
|
2979
|
+
recorded against the identity the deciding process was launched with, no
|
|
2980
|
+
channel enforces a mapping, and nothing is wrong. A FAIL where the policy
|
|
2981
|
+
maps senders for a channel and lists an approver on that channel with no id
|
|
2982
|
+
of their own there — that person's next tap is refused `sender-unmapped` and
|
|
2983
|
+
nothing is recorded, so the file says they may decide on a surface where they
|
|
2984
|
+
cannot. A PASS where every approver a mapped channel reaches carries an id
|
|
2985
|
+
there. The `fix` is `approval policy amend`, which owns the edit and the
|
|
2986
|
+
re-attestation it costs. The row reads the policy and nothing else: no log,
|
|
2987
|
+
no network, no credential, and it prints nobody's account id — the mapping is
|
|
2988
|
+
in a file the operator can open, and a health row is read over shoulders.
|
|
2989
|
+
- **codex-auto-reviewer** — has anything other than this gate answered a
|
|
2990
|
+
question this gate exists to ask (APRV-378)? It reads
|
|
2991
|
+
`audit.question_preempted` and nothing else. A FAIL where one landed in the
|
|
2992
|
+
last 24 hours, naming the source, the question in the other party's terms and
|
|
2993
|
+
their verdict; a PASS where none did, mentioning any older ones; a SKIP where
|
|
2994
|
+
the chain did not verify. A PASS says the log holds no such record and NOT
|
|
2995
|
+
that a harness auto-reviewer is off: whether it runs is configuration this
|
|
2996
|
+
runtime cannot read, and a row written against a guessed configuration key
|
|
2997
|
+
would find nothing and report green, which is the worst direction a health
|
|
2998
|
+
check can fail in. The `fix` points at the harness's own configuration,
|
|
2999
|
+
because nothing here can turn another system's reviewer off.
|
|
3000
|
+
|
|
3001
|
+
|
|
2625
3002
|
|
|
2626
3003
|
**`--json`** (one object on stdout):
|
|
2627
3004
|
|
|
@@ -2650,8 +3027,9 @@ reaches this backlog. A `supervised-live` class puts a declared `live_rate`
|
|
|
2650
3027
|
fraction of its actions through the human gate BEFORE they run; those are
|
|
2651
3028
|
ordinary manual requests with ordinary grants and tokens, a person has already
|
|
2652
3029
|
answered them, and they are not drawn a second time for retrospective review. A
|
|
2653
|
-
`supervised-retro` class — and the bare `supervised`, which is now
|
|
2654
|
-
|
|
3030
|
+
`supervised-retro` class — and the bare `supervised`, which is now a deprecated
|
|
3031
|
+
alias for it, still parsed and removed in a future schema version (APRV-335) —
|
|
3032
|
+
is what this page is about. `approval policy check` names the mode in its
|
|
2655
3033
|
final line and in `outcome.supervision`.
|
|
2656
3034
|
|
|
2657
3035
|
Supervised actions execute immediately and are audited afterwards. The daemon
|
|
@@ -3501,6 +3879,64 @@ leaves the sample exactly where it was: open, listed by `approval audit list`,
|
|
|
3501
3879
|
and reviewable with `approval audit review <seq>`. Nothing in this channel can
|
|
3502
3880
|
empty the backlog, which is the property a sampled-audit backlog exists to have.
|
|
3503
3881
|
|
|
3882
|
+
**A refused gesture leaves a record (APRV-355).** When the policy maps senders,
|
|
3883
|
+
a checkpoint signature or a review from an account the attested policy names
|
|
3884
|
+
nobody for is refused before any verb runs, and since this change the attempt is
|
|
3885
|
+
recorded: one **`audit.gesture_refused`**, with a `system:gate` actor, the
|
|
3886
|
+
channel, and a payload carrying `gesture`
|
|
3887
|
+
(`checkpoint-signature`, `review`, `review-note`), `code`
|
|
3888
|
+
(`sender-unmapped`, `sender-ambiguous`, `sender-key-unavailable`,
|
|
3889
|
+
`policy-not-attested`), the refusal
|
|
3890
|
+
`message`, the observed `sender`, and `actor` only where the runtime could name
|
|
3891
|
+
a person. Under a keyed mapping (APRV-370) the `sender.id` is the digest rather
|
|
3892
|
+
than the account, and `sender.hashed` is `true` beside it.
|
|
3893
|
+
|
|
3894
|
+
```
|
|
3895
|
+
{"event":"audit.gesture_refused","actor":"system:gate","channel":"telegram",
|
|
3896
|
+
"payload":{"gesture":"checkpoint-signature","code":"sender-unmapped",
|
|
3897
|
+
"sender":{"channel":"telegram","id":"5551234567"},"message":"…"}}
|
|
3898
|
+
```
|
|
3899
|
+
|
|
3900
|
+
It exists because the only refusal record before it,
|
|
3901
|
+
`audit.decision_refused`, requires an `action_key` and a `decision` of grant,
|
|
3902
|
+
reject or revoke, and a signature or a review has neither; writing one there
|
|
3903
|
+
would mean inventing both. The record is audit tier in the strict sense: it
|
|
3904
|
+
authorizes nothing, settles no request, charges no budget, is not sampled, and
|
|
3905
|
+
no enforcement path reads it. `approval log tail` and `approval log export`
|
|
3906
|
+
show it like any other record. Nothing about the refusal itself changed — no
|
|
3907
|
+
signature is appended and no review is recorded — and a listener whose policy
|
|
3908
|
+
maps no senders never reaches this path at all.
|
|
3909
|
+
|
|
3910
|
+
**A question answered by something other than the gate leaves a record
|
|
3911
|
+
(APRV-378).** Codex's app-server can resolve an approval with a model call
|
|
3912
|
+
before `approval codex bridge` is asked, and disclose it afterwards through an
|
|
3913
|
+
`item/autoApprovalReview` notification. The bridge stops the session on one
|
|
3914
|
+
(`bridge-auto-reviewer-active`), and it appends exactly one
|
|
3915
|
+
**`audit.question_preempted`** first, through the same append path as every
|
|
3916
|
+
other record:
|
|
3917
|
+
|
|
3918
|
+
```
|
|
3919
|
+
{"event":"audit.question_preempted","actor":"system:gate",
|
|
3920
|
+
"payload":{"source":"codex-auto-reviewer","verdict":"accept",
|
|
3921
|
+
"question":{"id":"item_01H9","method":"item/autoApprovalReview/completed",
|
|
3922
|
+
"thread":"thread_7f2","turn":"turn_3"}}}
|
|
3923
|
+
```
|
|
3924
|
+
|
|
3925
|
+
`source` is a closed set naming who answered, so the next system that does this
|
|
3926
|
+
gains a member rather than a type. `question` is the other party's own
|
|
3927
|
+
identifiers, because a record about their decision has to name it in their terms
|
|
3928
|
+
or a reader cannot go and find it on their side. `verdict` is their word
|
|
3929
|
+
verbatim and is ABSENT when the disclosure stated none: a record that said
|
|
3930
|
+
`accept` by default would be this runtime inventing somebody else's decision.
|
|
3931
|
+
|
|
3932
|
+
The record is audit tier in the strict sense: it authorizes nothing, settles no
|
|
3933
|
+
request, charges no budget, is not sampled, and no enforcement path reads it.
|
|
3934
|
+
`approval log tail` and `approval log export` show it like any other record, and
|
|
3935
|
+
`approval doctor`'s `codex-auto-reviewer` row reads it: fail when one landed in
|
|
3936
|
+
the last 24 hours, pass otherwise, and a pass says the log holds no such record
|
|
3937
|
+
rather than that any auto-reviewer is off. Nothing on this machine can say the
|
|
3938
|
+
latter, which is why the bridge probes (APRV-364) instead of reading a setting.
|
|
3939
|
+
|
|
3504
3940
|
**A settled request stops looking live.** Every terminal state the listener
|
|
3505
3941
|
observes for a message it sent edits that message: the text becomes the outcome
|
|
3506
3942
|
(`✓ APPROVED`, `✗ REJECTED`, `✗ REVOKED`, `✗ EXPIRED`, `WITHDRAWN`) with the
|
|
@@ -3647,6 +4083,20 @@ A HUMAN commits those files: they are `policy.edit`. `docs/agent-sdk-hook.md`
|
|
|
3647
4083
|
is the third caller: a Python Agent SDK application has no settings file, so it
|
|
3648
4084
|
spawns this same verb from a hook callback (APRV-242).
|
|
3649
4085
|
|
|
4086
|
+
**`hook grok` is the one exception to the exit codes above (APRV-243).** Grok
|
|
4087
|
+
Build reads exit 2 as the deny and exit 0 as the allow, whatever stdout said,
|
|
4088
|
+
so on that harness alone a deny is exit 2 with `{"decision":"deny","reason":…}`
|
|
4089
|
+
on stdout, and the post-execution event exits 0 in every case rather than
|
|
4090
|
+
using 2 for visibility. Its envelope is camelCase (`toolName`, `toolInput`,
|
|
4091
|
+
`sessionId`, `hookEventName`, plus a `workspaceRoot` this runtime ignores in
|
|
4092
|
+
favour of `cwd`); snake_case is still read and wins when both spellings are
|
|
4093
|
+
present. `approval hook grok --help` prints the `.grok/hooks/pre-tool-use.json`
|
|
4094
|
+
the human commits, and that entry's own `timeout` must exceed `--timeout`.
|
|
4095
|
+
Grok Build FAILS OPEN on hook timeout, crash and malformed output, with no
|
|
4096
|
+
setting to change it, which the fail-closed invariant does not survive;
|
|
4097
|
+
`docs/grok-hook.md` states which cases the adapter cannot cover and is worth
|
|
4098
|
+
reading before the file is committed.
|
|
4099
|
+
|
|
3650
4100
|
**Register the same command for the post-execution event too (APRV-145).** One
|
|
3651
4101
|
binary answers two events, dispatched on `hook_event_name`. A `PostToolUse` or
|
|
3652
4102
|
`PostToolUseFailure` run closes the delegated `execution.started` the
|
|
@@ -3679,8 +4129,11 @@ gate.self the "approval" CLI itself is pass-through
|
|
|
3679
4129
|
Bash (Claude Code) and Shell (Cursor) commands are classified into SPEC.md §7
|
|
3680
4130
|
action classes. A `git push` that names `refs/tags/*`, a bare `v`-prefixed
|
|
3681
4131
|
semantic-version-shaped tag, `tag <name>`, `--tags`, or `--follow-tags` is
|
|
3682
|
-
`release.publish`; force and mirror pushes remain `vcs.history.rewrite
|
|
3683
|
-
|
|
4132
|
+
`release.publish`; force and mirror pushes remain `vcs.history.rewrite`; a push
|
|
4133
|
+
that DELETES a remote ref — `--delete`, `-d`, or a colon refspec such as
|
|
4134
|
+
`:refs/heads/x` — is `vcs.ref.delete` with the ref names bound (APRV-352), and a
|
|
4135
|
+
tag deletion keeps `release.publish`; ordinary branch pushes retain their branch
|
|
4136
|
+
or trunk class. Claude file tools
|
|
3684
4137
|
(Edit, Write, MultiEdit, NotebookEdit) and
|
|
3685
4138
|
Cursor Write/Delete are gated only when the file is policy-protected
|
|
3686
4139
|
(`APPROVAL.md`, `.approval/`, `CLAUDE.md`, `AGENTS.md`, `.claude/settings*`,
|
|
@@ -3698,8 +4151,21 @@ its own flags are not parsed as this verb's.
|
|
|
3698
4151
|
no human is asked. This union's spelling of the gate's `class-human-only`,
|
|
3699
4152
|
which the detail names in full. The opposite repair to `hook-unclassified`:
|
|
3700
4153
|
that one says declare a class, this one says a person runs the command.
|
|
4154
|
+
- `hook-harness-launch-unruled` — some class of the command is in the
|
|
4155
|
+
`harness.launch.*` family and this policy names no rule for it (APRV-354).
|
|
4156
|
+
The family resolves only under an explicit rule, `harness.launch.*` or
|
|
4157
|
+
`harness.launch.NAME`, and never under `defaults.autonomy`, because a grant
|
|
4158
|
+
of the class covers the launch and nothing the launched session then does.
|
|
4159
|
+
Distinct from `hook-unclassified` (the classifier had nothing to say; here it
|
|
4160
|
+
was clear and the policy is silent) and from `hook-class-human-only` (the
|
|
4161
|
+
policy has spoken and reserved the class; the repair there is for a person to
|
|
4162
|
+
run the command, and here it is to write a line).
|
|
3701
4163
|
- `hook-opaque` — a construct whose effect cannot be read from the text
|
|
3702
|
-
(`
|
|
4164
|
+
(`eval`, `xargs`, backticks, a non-read substitution). A login shell around
|
|
4165
|
+
ONE inline script is classified by that script since APRV-380, so
|
|
4166
|
+
`zsh -lc 'git push origin main'` is `vcs.push.main`; a script file, an extra
|
|
4167
|
+
word, a redirection on the wrapper, an assignment prefix and a nested shell
|
|
4168
|
+
all stay opaque.
|
|
3703
4169
|
- `hook-unparseable` — the command line could not be tokenized.
|
|
3704
4170
|
- `hook-rejected` — a human said no.
|
|
3705
4171
|
- `hook-revoked` — a granted approval was withdrawn.
|
|
@@ -3778,8 +4244,10 @@ importer collects the bullets under those headings into a second draft fence,
|
|
|
3778
4244
|
` ```yaml approval-values ` (SPEC.md §5.3), printed after the policy draft on
|
|
3779
4245
|
stdout and written after it with `--out`; `--json` carries it as
|
|
3780
4246
|
`values_draft`, or `null` when no such heading exists. Every bullet lands in
|
|
3781
|
-
`
|
|
3782
|
-
|
|
4247
|
+
`like`, the middle grade, and none in `love` or `dislike`: how strongly a line
|
|
4248
|
+
is meant is the human's to say, and an importer that reached for the strongest
|
|
4249
|
+
grade would be putting words in their mouth. (`like` is where what an operator
|
|
4250
|
+
asks for lives since APRV-336 folded `wants` into it.) A
|
|
3783
4251
|
bullet over the schema's 200 characters is truncated with a warning rather than
|
|
3784
4252
|
dropped, and bullets past the twentieth are kept as comments inside the fence,
|
|
3785
4253
|
which is the same stance the permissions half takes on unmapped bullets. The
|
|
@@ -3911,6 +4379,19 @@ in the file said what the operator wanted the work to be like. The optional
|
|
|
3911
4379
|
` ```yaml approval-values ` block (SPEC.md §5.3) is that, and this verb prints
|
|
3912
4380
|
it.
|
|
3913
4381
|
|
|
4382
|
+
**The keys.** `version`, the quoted string `"0.2"` and the only required one;
|
|
4383
|
+
the standing grades `love`, `like` and `dislike`, printed as `loves:`, `likes:`
|
|
4384
|
+
and `dislikes:`; and `communication`, one sentence or two on how the operator
|
|
4385
|
+
reads and answers, printed under `communication:`. APRV-336 settled that shape:
|
|
4386
|
+
a `wants` list for what the operator asks of an agent folded into `like`, since
|
|
4387
|
+
a request about behaviour and a preference about the output are graded by the
|
|
4388
|
+
same person in the same way and one list is easier to keep true, and `responds`
|
|
4389
|
+
became `communication`, which no longer reads as a sibling of `approval
|
|
4390
|
+
feedback`. A block written to the earlier format (`version: 1`, a `wants` list)
|
|
4391
|
+
is refused with the code `version-unsupported` and a message naming both edits,
|
|
4392
|
+
rather than with a schema violation a reader has to decode. The same code
|
|
4393
|
+
answers `version: 0.2` written without quotes, which YAML reads as a float.
|
|
4394
|
+
|
|
3914
4395
|
**It is guidance, and it is never policy.** Every output form opens with the
|
|
3915
4396
|
banner saying so, `--json` carries the same sentence in `note`, and the reason
|
|
3916
4397
|
is the same discipline `journal read` applies in the opposite direction: a
|
|
@@ -4191,7 +4672,22 @@ the clock starts when the daemon starts), and at a clean shutdown when records
|
|
|
4191
4672
|
are still owed. Every attempt goes through the gate as `agent:daemon`: the cycle
|
|
4192
4673
|
registers, requests, and proceeds only where the policy lets it, so a
|
|
4193
4674
|
`supervised-live` draw that selects the advance, or a class that resolves
|
|
4194
|
-
`manual`, stops it with nothing committed and the question in the queue.
|
|
4675
|
+
`manual`, stops it with nothing committed and the question in the queue.
|
|
4676
|
+
|
|
4677
|
+
**Which class it asks under, and which actor may advance autonomously
|
|
4678
|
+
(APRV-382).** Two classes, and the running process picks between them.
|
|
4679
|
+
`log.advance.daemon` is the daemon's own, asked only by the cadence advance
|
|
4680
|
+
inside `approval daemon run` and `approval up`; `log.advance` is what every
|
|
4681
|
+
other actor asks under, a session in a worktree and a human terminal alike. A
|
|
4682
|
+
policy may hold the daemon's class `autonomous` (this repository's does, since
|
|
4683
|
+
the advance publishes records the log already holds, appends nothing and decides
|
|
4684
|
+
nothing) while leaving the base class where it was. Nothing an agent can type
|
|
4685
|
+
reaches the looser line: `approval log advance` classifies `log.advance`
|
|
4686
|
+
whoever runs it. The choice is read from the process rather than from any
|
|
4687
|
+
argument, a policy that declares no rule for the daemon's class leaves the
|
|
4688
|
+
cadence gated exactly as it was, and a cycle that is NOT the daemon's whose
|
|
4689
|
+
class resolves `autonomous` is refused `advance-actor-not-daemon` before
|
|
4690
|
+
anything is appended. A gated
|
|
4195
4691
|
or failed attempt is an `advance` line plus an `advance-refused` warning, and the
|
|
4196
4692
|
next tick tries again — the cadence interval is the retry bound, so a refusal
|
|
4197
4693
|
never loops. One records branch and ONE PULL REQUEST PER DAY: the first advance
|
|
@@ -4483,6 +4979,46 @@ Stop `up` before running Telegram setup or a standalone listener for its bot.
|
|
|
4483
4979
|
One bot must have one polling runtime. After setup changes, reload the instance
|
|
4484
4980
|
environment before starting `up` again.
|
|
4485
4981
|
|
|
4982
|
+
**Two refusals keep one bot to one instance (APRV-390).** Both are made before
|
|
4983
|
+
the daemon's first tick and before the first poll, and both exit 1 with a
|
|
4984
|
+
machine-readable code on stderr:
|
|
4985
|
+
|
|
4986
|
+
| code | what it means |
|
|
4987
|
+
| --- | --- |
|
|
4988
|
+
| `cross-instance-credential` | A credential variable holds a value this instance did not configure: an `.approval/env` line naming another instance's keystore item, or an export whose value is not what this instance's file resolves to today. The message carries the `unset` line that fixes it. |
|
|
4989
|
+
| `bot-owned-elsewhere` | One `getMe`, before any `getUpdates`, named a bot another instance on this machine has already claimed. The message names that instance's directory. |
|
|
4990
|
+
|
|
4991
|
+
`--allow-cross-instance` overrides both, starts, and prints on every run what it
|
|
4992
|
+
is overriding. Until APRV-390 the first of these was a warning printed on the
|
|
4993
|
+
way past a runtime that had already started, which is how a demo gate spent an
|
|
4994
|
+
evening polling the primary's bot.
|
|
4995
|
+
|
|
4996
|
+
The first refusal COMPARES VALUES rather than only reading names, because it is
|
|
4997
|
+
the one check that is about to use the value. That closes the case names cannot
|
|
4998
|
+
see: `approval env` never overrides a variable this shell has already exported,
|
|
4999
|
+
so a token re-stored in the keystore does not reach a terminal holding the old
|
|
5000
|
+
one, and the daemon answers 401 while the same line read by hand passes `getMe`.
|
|
5001
|
+
It also removes a false positive — an export whose value IS what the file
|
|
5002
|
+
resolves to is correct however it got there, so the documented `eval "$(approval
|
|
5003
|
+
env)"` is not a finding. `approval doctor` keeps the name-only rule; a
|
|
5004
|
+
diagnostic may not block on a keystore-unlock dialog. No value is printed on any
|
|
5005
|
+
path.
|
|
5006
|
+
|
|
5007
|
+
The `getMe` result is recorded against the instance in
|
|
5008
|
+
`.approval/channel-owner.json` (gitignored) and in a per-machine registry under
|
|
5009
|
+
the platform's user state directory, `approval/bots.json`. `APPROVAL_STATE_DIR`
|
|
5010
|
+
relocates that registry. A bot is identified by its id AND its Bot API base,
|
|
5011
|
+
because an id is unique within one deployment and nothing more: two gates
|
|
5012
|
+
pointed at two different `--api-base` values hold two different bots and neither
|
|
5013
|
+
refuses the other. Neither file is evidence, nothing reads them to widen a
|
|
5014
|
+
permission, and losing them costs one `getMe`. An unreachable Bot API is not a
|
|
5015
|
+
refusal: it says so and starts, because a captive portal is also a `getUpdates`
|
|
5016
|
+
that cannot conflict with anything.
|
|
5017
|
+
|
|
5018
|
+
A 409 that still happens at runtime — another machine, or a poller started
|
|
5019
|
+
outside this runtime — is reported once, with the instances the registry knows
|
|
5020
|
+
about, and its repeats are counted rather than reprinted.
|
|
5021
|
+
|
|
4486
5022
|
**The ambient runtime: the daemon loop and every configured channel in one
|
|
4487
5023
|
supervised foreground process.** `approval daemon run --with-channels` is the
|
|
4488
5024
|
same verb spelled from the other side, and it reaches the same function before
|
|
@@ -4505,13 +5041,15 @@ because `git status` does not say what the upstream range changed. So the verb
|
|
|
4505
5041
|
does all four, and `approval daemon run` runs the identical preflight from the
|
|
4506
5042
|
identical module, printing the identical two lines.
|
|
4507
5043
|
|
|
4508
|
-
It is allowed exactly
|
|
5044
|
+
It is allowed exactly four writes: a `--ff-only` merge, `npm run build`,
|
|
4509
5045
|
clearing an untracked `backlog/tasks/` file the incoming commit already contains
|
|
4510
|
-
out of the merge's way (APRV-300, described below)
|
|
5046
|
+
out of the merge's way (APRV-300, described below), and the reconcile `approval
|
|
5047
|
+
log sync` performs on its behalf (APRV-346, described below). It
|
|
4511
5048
|
never resets, never stashes, never checks anything out, and never touches the
|
|
4512
|
-
working log. That list is not caution for its own sake: a working
|
|
4513
|
-
rewound through git underneath a live appender is fork 2 of
|
|
4514
|
-
incident `approval log sync` exists to prevent
|
|
5049
|
+
working log itself. That list is not caution for its own sake: a working
|
|
5050
|
+
`events.jsonl` rewound through git underneath a live appender is fork 2 of
|
|
5051
|
+
2026-08-20, the incident `approval log sync` exists to prevent, which is why the
|
|
5052
|
+
one write that does move that file is made by that verb and by nothing here.
|
|
4515
5053
|
|
|
4516
5054
|
**Safe** means both of: this checkout is not AHEAD of the remote, and no path the
|
|
4517
5055
|
upstream range changes is locally modified. When it is safe, the preflight
|
|
@@ -4521,11 +5059,45 @@ running. When it is not, it refuses, and changes nothing:
|
|
|
4521
5059
|
| code | fires when | next |
|
|
4522
5060
|
|---|---|---|
|
|
4523
5061
|
| `up-preflight-behind-ahead` | `origin/<branch>..HEAD` is non-empty: this checkout carries commits the remote has never seen. A fast-forward is not the operation for that state, and choosing a side is a decision. | look at them (`git log --oneline origin/main..HEAD`), then push them or `git reset --keep` |
|
|
4524
|
-
| `up-preflight-log-diverged` | the upstream range rewrites `.approval/log/events.jsonl` or `.approval/QUEUE.md`,
|
|
5062
|
+
| `up-preflight-log-diverged` | the upstream range rewrites `.approval/log/events.jsonl` or `.approval/QUEUE.md`, this working copy has uncommitted changes to one of them, and the two chains are **not** in a prefix relationship (or cannot be compared at all). The judgment a human could not make by eye. A working log that merely extends the committed one is reconciled instead, see below. | `approval log sync` |
|
|
4525
5063
|
| `up-preflight-dirty-protected` | some other path the upstream range changes is locally modified, so `git merge --ff-only` would refuse rather than overwrite it. | look at the diff, or `approval up --no-preflight` |
|
|
4526
5064
|
| `up-preflight-task-file-conflict` | an untracked file under `backlog/tasks/` stopped the fast-forward and it holds lines the incoming copy does not. Which version is wanted is a question, and no verb here will pick. | read the two copies, move yours aside, run `approval up` again |
|
|
4527
5065
|
| `up-preflight-failed` | a write the preflight attempted did not complete: the fast-forward, or the rebuild. Not a judgment, so it is not in the union above; the message names the step, and for a build it names the exit code `npm run build` came back with. | `npm run build` to see the whole error, or `approval up --no-build` if you mean to run the stale one |
|
|
4528
5066
|
|
|
5067
|
+
**A working log that merely extends the committed one is reconciled, not
|
|
5068
|
+
refused (APRV-346).** Every records advance moves `origin/main`'s
|
|
5069
|
+
`.approval/log/events.jsonl` while the hook keeps appending locally, so
|
|
5070
|
+
"upstream changed the log and so did this working copy" is the normal state of
|
|
5071
|
+
the primary checkout after a merge. Refusing it sent the operator to `approval
|
|
5072
|
+
log sync` and then back to `approval up`, every time. So the collision is a
|
|
5073
|
+
question now: `core/log-reconcile.ts` — the same comparison `log sync` and
|
|
5074
|
+
doctor's `log-drift` row use — is asked how the working chain stands to the
|
|
5075
|
+
committed chain at the fetched tip, and:
|
|
5076
|
+
|
|
5077
|
+
- **`ahead`, `behind` or `equal`** — one chain contains the other whole, so
|
|
5078
|
+
adopting the longer one extends and rewinds nothing. The preflight calls
|
|
5079
|
+
`approval log sync` itself, which holds the append lock for its whole ceremony
|
|
5080
|
+
(snapshot, baseline, fast-forward, reconcile, rebuild the projections,
|
|
5081
|
+
post-verify), and prints one line: `synced: fast-forwarded to origin/main
|
|
5082
|
+
<sha>, kept K local records`. The `--json` stream carries it as a
|
|
5083
|
+
`preflight_sync` event and the `preflight` line's `log_synced` reads true.
|
|
5084
|
+
Nothing here reimplements any of that ceremony; it supplies a caller for it;
|
|
5085
|
+
- **`diverged`** — two appenders built different records on one predecessor.
|
|
5086
|
+
Hash chains do not merge, so this is the `up-preflight-log-diverged` refusal
|
|
5087
|
+
above, unchanged, and it is now the **only** case that needs a hand-run
|
|
5088
|
+
`approval log sync` (which will tell you the same thing, at more length).
|
|
5089
|
+
|
|
5090
|
+
Four conditions have to hold before the reconcile is even offered, and each of
|
|
5091
|
+
them answers "refuse" rather than "probably fine": the log is the repository's
|
|
5092
|
+
own `.approval/log/events.jsonl` (not some other file named with `--log`), no
|
|
5093
|
+
*other* path the upstream range touches is locally modified, both chains verify
|
|
5094
|
+
clean, and the relation is a prefix one. A `log sync` that refuses anyway — an
|
|
5095
|
+
appender that took the lock first (`log-sync-locked`), a fork that landed
|
|
5096
|
+
between the read and the ceremony, a git failure — comes back as the same
|
|
5097
|
+
`up-preflight-log-diverged` refusal with the sync's own code and sentence in
|
|
5098
|
+
YOUR STATE. Nothing starts, and the working log is exactly as `log sync` found
|
|
5099
|
+
it.
|
|
5100
|
+
|
|
4529
5101
|
**An untracked task file no longer stops it (APRV-300).** A lane files
|
|
4530
5102
|
`backlog/tasks/aprv-299` on its branch and its pull request merges, while the
|
|
4531
5103
|
primary checkout holds the same path untracked from its own `backlog task
|
|
@@ -5342,6 +5914,51 @@ disables it whenever the policy names no variable, and this verb does not edit a
|
|
|
5342
5914
|
attested policy file. It prints the block to add and the `approval policy amend`
|
|
5343
5915
|
ceremony that attests it.
|
|
5344
5916
|
|
|
5917
|
+
## setup sender-key
|
|
5918
|
+
|
|
5919
|
+
The operator-held key that turns a channel account id into the value an
|
|
5920
|
+
`approvers[id].senders` mapping carries (APRV-370). It exists for the deployment
|
|
5921
|
+
that PUBLISHES its policy and its log, where the raw account id is disclosed
|
|
5922
|
+
once in the file and then on every decision.
|
|
5923
|
+
|
|
5924
|
+
A plain unkeyed digest would not fix that. A Telegram account id is a ten-digit
|
|
5925
|
+
decimal number, the whole space is enumerable on a laptop, and a digest anybody
|
|
5926
|
+
can reverse states a privacy property it does not have. So the mapping value is
|
|
5927
|
+
`hmac-sha256:<hex>`, computed under this key, which is in the environment and
|
|
5928
|
+
never in the policy file.
|
|
5929
|
+
|
|
5930
|
+
Bare, the verb mints, stores and records the key, exactly as `setup sampling`
|
|
5931
|
+
does with its secret, and edits no policy file. The value is not printed and
|
|
5932
|
+
there is no verb that prints it.
|
|
5933
|
+
|
|
5934
|
+
With `--id <account-id>` it mints nothing and stores nothing. It reads the key
|
|
5935
|
+
from the environment, prints the `hmac-sha256:<hex>` for that account, and
|
|
5936
|
+
prints the `senders` line and a paste-ready proposal pair around it. That is the
|
|
5937
|
+
one thing an agent writing a policy proposal cannot do — the digest depends on a
|
|
5938
|
+
secret only the operator's machine holds — so the proposal page says to run it
|
|
5939
|
+
rather than carrying a placeholder:
|
|
5940
|
+
|
|
5941
|
+
```sh
|
|
5942
|
+
approval setup sender-key # once, interactive, human-only
|
|
5943
|
+
eval "$(approval env)"
|
|
5944
|
+
approval setup sender-key --id 7345216485
|
|
5945
|
+
```
|
|
5946
|
+
|
|
5947
|
+
Because it stores nothing and prints a value designed to be published, `--id` is
|
|
5948
|
+
the one `setup` path that runs without a terminal and accepts `--json`.
|
|
5949
|
+
|
|
5950
|
+
**The key is not an authenticator.** Nothing about the gate's safety rests on
|
|
5951
|
+
its secrecy: somebody who learns it learns which account ids a policy names,
|
|
5952
|
+
which is what the raw form told everybody. What it buys is that the published
|
|
5953
|
+
policy and the published log stop carrying the account.
|
|
5954
|
+
|
|
5955
|
+
**A listener that holds no key under a keyed mapping decides nothing** on that
|
|
5956
|
+
channel. Every tap is refused `sender-key-unavailable`, and there is no fallback
|
|
5957
|
+
to comparing the observed id against the raw entries: without the key the
|
|
5958
|
+
runtime cannot tell whether the account is also claimed by a keyed approver, so
|
|
5959
|
+
it cannot run the ambiguity check the mapping rests on. `approval doctor`'s
|
|
5960
|
+
`sender-mapping` row names the form in use and says whether the key resolves.
|
|
5961
|
+
|
|
5345
5962
|
## setup checkpoint
|
|
5346
5963
|
|
|
5347
5964
|
Mints the Ed25519 keypair a human signs the log's head with (APRV-220's record,
|
|
@@ -5551,6 +6168,28 @@ a `human:<id>` and an `agent:` or `system:` actor is refused at exit 2. Exit 1
|
|
|
5551
6168
|
means the far end refused: an invalid token, a 409 from a running listener, or no
|
|
5552
6169
|
message reaching the bot before the deadline.
|
|
5553
6170
|
|
|
6171
|
+
**One bot per instance (APRV-390), and both names are derived.** The keystore
|
|
6172
|
+
item this verb creates is `approval-tg-token-<instance id>`, where the instance
|
|
6173
|
+
id is the eight hex digits `approval doctor` prints in its `keychain-scope` row,
|
|
6174
|
+
so two gates on one machine store two items. An operator who prefers a readable
|
|
6175
|
+
suffix may write their own name into `.approval/env` instead; nothing is
|
|
6176
|
+
migrated and `approval env --check` reports which item each variable resolves
|
|
6177
|
+
through. The variable is whatever the policy's
|
|
6178
|
+
`channels.telegram.token_env` declares; a policy that declares nothing gets
|
|
6179
|
+
`APPROVAL_TG_TOKEN`, which every other silent policy on the machine also gets,
|
|
6180
|
+
so a second gate on one machine declares a pair of its own (the packaged demo
|
|
6181
|
+
policy declares `APPROVAL_DEMO_TG_TOKEN` and `APPROVAL_DEMO_TG_CHAT`).
|
|
6182
|
+
|
|
6183
|
+
The `getMe` that proves the token also says which bot it is, and this verb
|
|
6184
|
+
records that against the instance in `.approval/channel-owner.json` (gitignored)
|
|
6185
|
+
and in a per-machine registry under the platform's user state directory,
|
|
6186
|
+
`approval/bots.json`. A bot another local instance has already claimed is
|
|
6187
|
+
refused before anything is written, naming that instance's directory; the values
|
|
6188
|
+
are open names and never a token. `--allow-cross-instance` records this instance
|
|
6189
|
+
as an owner anyway and says what it is doing. An instance set up before this
|
|
6190
|
+
existed keeps working: nothing is migrated, and the first `approval up` or
|
|
6191
|
+
listener start writes the record from its own `getMe`.
|
|
6192
|
+
|
|
5554
6193
|
## setup service
|
|
5555
6194
|
|
|
5556
6195
|
**It writes one file: the launchd user agent or the systemd user unit that runs
|
|
@@ -5710,7 +6349,268 @@ the broker and runner present? POSIX ownership does not establish ACL custody,
|
|
|
5710
6349
|
so this slice executes no manifest-selected binary and reports runtime versions
|
|
5711
6350
|
unchecked. Unknown evidence is a refusal.
|
|
5712
6351
|
|
|
5713
|
-
The first slice deliberately
|
|
6352
|
+
The first slice deliberately made start and serve return codex-not-ready. An
|
|
5714
6353
|
npm install, generated config, or passing bundle check does not create a
|
|
5715
6354
|
mandatory boundary. The whole approval codex family is operator-only and absent
|
|
5716
6355
|
from the ordinary broad MCP catalog.
|
|
6356
|
+
|
|
6357
|
+
### The workspace broker (APRV-325.2)
|
|
6358
|
+
|
|
6359
|
+
approval codex apply is the executable half. It takes an instance manifest and a
|
|
6360
|
+
proposal file, and the split between them is the design: the manifest supplies
|
|
6361
|
+
the acting identity (agent:codex-<instance_id>), the workspace root, the policy
|
|
6362
|
+
file and the log, and the proposal supplies operations and the SHA-256 of the
|
|
6363
|
+
policy it was built against. A proposal naming anything else, an actor, a root,
|
|
6364
|
+
a class, a token, a sandbox posture, is refused input-invalid rather than having
|
|
6365
|
+
the extra key ignored, because a caller that wrote one meant something by it.
|
|
6366
|
+
|
|
6367
|
+
The order of one apply, and why each step is where it is:
|
|
6368
|
+
|
|
6369
|
+
1. Read the log under chain verification, read the policy once, hash those exact
|
|
6370
|
+
bytes, and check them against the latest attestation. An unattested or
|
|
6371
|
+
drifted policy refuses here, before a plan exists.
|
|
6372
|
+
2. Compare the caller's expected digest. A mismatch is attestation-drift: the
|
|
6373
|
+
proposal was built against a policy nobody is enforcing.
|
|
6374
|
+
3. Plan through src/codex/workspace-plan.ts, which does the path work: traversal,
|
|
6375
|
+
symlinks, hardlinks, case aliases, missing sources, existing destinations,
|
|
6376
|
+
preimage digests, and human-only classes refused before any preimage is read.
|
|
6377
|
+
4. Register one action per distinct path class under a task id derived from the
|
|
6378
|
+
payload hash. Classes are never collapsed: the class is what the operator's
|
|
6379
|
+
roster, budget and autonomy are keyed to.
|
|
6380
|
+
5. Authorize every leg. A leg the policy sends to a human refuses
|
|
6381
|
+
approval-required and names the action keys to grant; nothing is written, and
|
|
6382
|
+
the same proposal applies once a person has decided.
|
|
6383
|
+
6. Start every leg before any byte moves. A later leg refusing to start closes
|
|
6384
|
+
the earlier ones execution.failed and leaves the workspace untouched by
|
|
6385
|
+
construction rather than by cleanup.
|
|
6386
|
+
7. Take the workspace lock, then revalidate the plan under it. A revalidation
|
|
6387
|
+
that precedes the lock proves only what was true before another writer could
|
|
6388
|
+
act.
|
|
6389
|
+
8. Stage the new bytes and the preimages on the same filesystem, fsync them,
|
|
6390
|
+
write a journal naming the before-state and the after-state, fsync that, and
|
|
6391
|
+
only then apply.
|
|
6392
|
+
9. Read the workspace back. All-after completes every leg, all-before fails every
|
|
6393
|
+
leg, and anything else, a partial apply, a failed rollback, an endpoint that
|
|
6394
|
+
cannot be read, records execution.indeterminate with the reason
|
|
6395
|
+
workspace-commit-unknown on every leg and retains the journal and the lock.
|
|
6396
|
+
|
|
6397
|
+
approval codex recover reads that retained journal and reports before, after or
|
|
6398
|
+
mixed. It repairs nothing, and the restraint is the point: rolling a mixed
|
|
6399
|
+
workspace forward would guess which half the human approved, and rolling it back
|
|
6400
|
+
would delete the half that committed. Exit 1 means mixed. The resolution is a
|
|
6401
|
+
person's, through approval execution reconcile.
|
|
6402
|
+
|
|
6403
|
+
Custody is claimed only as far as the platform proves it. The broker takes an
|
|
6404
|
+
O_CREAT|O_EXCL lock, which excludes other cooperating brokers and nothing else,
|
|
6405
|
+
and then asks POSIX ownership and mode whether any other principal can write the
|
|
6406
|
+
directories it is about to touch. It reports os-exclusive only when both hold and
|
|
6407
|
+
advisory otherwise, and it always reports acl-unproven, because ownership and
|
|
6408
|
+
mode say nothing about ACLs. --require-exclusive-custody turns the weak answer
|
|
6409
|
+
into a refusal rather than a footnote.
|
|
6410
|
+
|
|
6411
|
+
approval codex serve publishes that broker over stdio as exactly one MCP tool,
|
|
6412
|
+
codex_workspace_apply, checked at call time as well as at list time. It is a
|
|
6413
|
+
different server from approval mcp serve, whose catalog is the whole verb
|
|
6414
|
+
registry and therefore grows: a constrained session has to reach one door, and
|
|
6415
|
+
the same door next month.
|
|
6416
|
+
|
|
6417
|
+
### The confined session (APRV-325.3)
|
|
6418
|
+
|
|
6419
|
+
approval codex start prepares the room a session's shell runs in. With no
|
|
6420
|
+
-- <command> it reports the room and runs nothing, which is what an operator
|
|
6421
|
+
checking a host should not have to start Codex to learn. With one, it runs that
|
|
6422
|
+
command inside the room and exits with the child's own code.
|
|
6423
|
+
|
|
6424
|
+
The shell gets a disposable workspace under the system temporary directory, and
|
|
6425
|
+
that workspace is the only path it may write. The canonical workspace is
|
|
6426
|
+
readable and never writable. Reads are jailed to exactly those two roots
|
|
6427
|
+
(APRV-347), so the gate home, other repositories and everything else the
|
|
6428
|
+
operator's home holds are unreadable whether or not anyone thought to name them,
|
|
6429
|
+
and the credential denials are emitted after the jail's allows so they remain
|
|
6430
|
+
the last word on the vault, the environment map and the sealing keys. The
|
|
6431
|
+
environment is an allow-list rather than a filtered copy of the operator's, so a
|
|
6432
|
+
provider key nobody taught this runtime about is absent rather than forgotten.
|
|
6433
|
+
Outbound network is denied, loopback included. Descendants inherit all of it,
|
|
6434
|
+
which is the property that matters: a session does not write files by calling
|
|
6435
|
+
into this runtime, it spawns shells that do.
|
|
6436
|
+
|
|
6437
|
+
There is no opt-out flag and no unwrapped fallback. approval run has
|
|
6438
|
+
--no-sandbox because a human's grant over exact bytes is authority to reach the
|
|
6439
|
+
world; a confined session has no such authority to present, so a host with no
|
|
6440
|
+
sandbox mechanism refuses (sandbox-unsupported) where approval run would record
|
|
6441
|
+
unsupported and proceed. A command that cannot be resolved on the session PATH is
|
|
6442
|
+
refused rather than spawned outside the room.
|
|
6443
|
+
|
|
6444
|
+
The disposable workspace is removed when the session ends, so a replay of the
|
|
6445
|
+
same shell work starts from an empty room. Only a brokered change survives it,
|
|
6446
|
+
which is the whole arrangement: the shell cannot reach the canonical workspace,
|
|
6447
|
+
and approval codex apply is how a change that a policy admitted does.
|
|
6448
|
+
|
|
6449
|
+
### The app-server bridge (APRV-361)
|
|
6450
|
+
|
|
6451
|
+
```
|
|
6452
|
+
approval codex bridge --prompt <text> [--workspace <dir>] [-- <server command>]
|
|
6453
|
+
```
|
|
6454
|
+
|
|
6455
|
+
approval codex bridge starts `codex app-server` and answers every approval
|
|
6456
|
+
request it raises through the policy and the log. Each
|
|
6457
|
+
`item/commandExecution/requestApproval` carries `command` and `cwd` on the same
|
|
6458
|
+
frame, minted by the harness runtime, and those two fields are exactly the pair
|
|
6459
|
+
the native hook lacks: a Codex `Bash` pre-event names only the command, so
|
|
6460
|
+
`approval hook codex` refuses every shell call as
|
|
6461
|
+
`hook-unsupported-execution-context` rather than bind bytes whose directory it
|
|
6462
|
+
does not know. Here the directory arrives with the question.
|
|
6463
|
+
|
|
6464
|
+
It reuses the hook's decision path rather than a copy of it. The request becomes
|
|
6465
|
+
the hook's own input (tool `Bash`, `tool_input.command` the string the server
|
|
6466
|
+
sent, `cwd` the directory it named) and goes through the same classifier, the
|
|
6467
|
+
same human-only refusal, the same sandbox requirement, the same loop floor and
|
|
6468
|
+
unattended guard, and the same register, request and wait against the verified
|
|
6469
|
+
view. What differs is where the answer goes: `{id, result: {decision}}` on the
|
|
6470
|
+
connection instead of a decision object on stdout.
|
|
6471
|
+
|
|
6472
|
+
The deadline is the policy's `approval_ttl`, not a harness ceiling. Every hook
|
|
6473
|
+
adapter answers inside a timeout its harness sets, and the retry grace exists so
|
|
6474
|
+
a denial-by-deadline is recoverable; this transport has no timeout at all, so a
|
|
6475
|
+
human who answers in eleven minutes is answering rather than arriving too late.
|
|
6476
|
+
`--wait` overrides it.
|
|
6477
|
+
|
|
6478
|
+
It answers accept or decline only, in the vocabulary the request advertised
|
|
6479
|
+
through `availableDecisions`, matched exactly and never by prefix, so
|
|
6480
|
+
`acceptWithExecpolicyAmendment` is not read as an accept. It never sends
|
|
6481
|
+
`acceptForSession` (standing authority for a whole session is a grant shape this
|
|
6482
|
+
project does not have), `cancel` or `abort` (those mean "stop the turn", and
|
|
6483
|
+
sending one would record an interruption as a denial). A request advertising
|
|
6484
|
+
nothing gets `accept` or `decline` and the report says the word was this
|
|
6485
|
+
runtime's own.
|
|
6486
|
+
|
|
6487
|
+
The vocabulary is eight spellings of those two words (`accept`, `approved`,
|
|
6488
|
+
`approve`, `allow`; `decline`, `denied`, `deny`, `reject`), and since APRV-367
|
|
6489
|
+
it is a type rather than a convention: the reply value cannot be constructed
|
|
6490
|
+
outside the list, one function turns a decision into bytes and re-checks
|
|
6491
|
+
membership there, and a word that somehow failed that check would be sent as a
|
|
6492
|
+
decline, since the only safe substitute for a word you cannot name is no. The
|
|
6493
|
+
match is case-insensitive, and what goes on the wire is this runtime's own
|
|
6494
|
+
spelling of the matched word. The `bridge-decisions` conformance suite pins the
|
|
6495
|
+
behaviour for a second implementation.
|
|
6496
|
+
|
|
6497
|
+
Its own refusals, beside the gate's:
|
|
6498
|
+
|
|
6499
|
+
```
|
|
6500
|
+
bridge-request-unbound no command, no cwd, or no call identity on the request
|
|
6501
|
+
bridge-command-unbound a command string that names no argv this client can bind (APRV-362)
|
|
6502
|
+
bridge-file-change-unbound an item-based file change whose content this client cannot
|
|
6503
|
+
produce from the item/started frame its item id names: no such
|
|
6504
|
+
frame, an item that is not a fileChange, an empty change set, or
|
|
6505
|
+
a frame belonging to another thread or turn (APRV-379)
|
|
6506
|
+
bridge-file-change-already-completed
|
|
6507
|
+
item/completed for that item arrived before the question, so the
|
|
6508
|
+
change had finished before this client was asked (APRV-379)
|
|
6509
|
+
bridge-unknown-request a server request this client has no reading for
|
|
6510
|
+
```
|
|
6511
|
+
|
|
6512
|
+
The **item-based file change is correlated, not guessed** (APRV-379). That API
|
|
6513
|
+
puts the content on an earlier `item/started` notification and the approval
|
|
6514
|
+
request refers to it by `itemId`, so the bridge keeps every item the thread
|
|
6515
|
+
announces and decides the request against the frame that id names: the paths
|
|
6516
|
+
take their classes, the payload binds the change set verbatim with
|
|
6517
|
+
`content_sha256` over it as received, and nothing parses the `diff`. The request
|
|
6518
|
+
carries no directory, so the paths resolve against the workspace the bridge
|
|
6519
|
+
named on `thread/start`, and one landing outside it is refused `hook-io`.
|
|
6520
|
+
|
|
6521
|
+
The **exec request binds words, not a rendering** (APRV-362). The item-based API
|
|
6522
|
+
delivers the command as one string, joined from the argv Codex will run, so the
|
|
6523
|
+
bridge un-joins it and the registered payload carries the string that arrived
|
|
6524
|
+
and the argv beside it. A string that is not readable as a join (an unterminated
|
|
6525
|
+
quote, a double quote outside a quoted run, a trailing backslash, or separation
|
|
6526
|
+
no join produces) is `bridge-command-unbound`, because approving it would
|
|
6527
|
+
approve this client's own re-parse. Byte equality with a re-rendering is
|
|
6528
|
+
deliberately not required: a join written for shell safety quotes more than this
|
|
6529
|
+
one does, and demanding equality would refuse ordinary traffic over a quoting
|
|
6530
|
+
rule nothing here records. A legacy argv array is rendered word by word rather
|
|
6531
|
+
than concatenated, so `["bash", "-lc", "rm -rf build"]` reaches the classifier
|
|
6532
|
+
as `bash -lc 'rm -rf build'` and not as five separate words.
|
|
6533
|
+
|
|
6534
|
+
A **legacy `applyPatchApproval` is decided rather than declined** (APRV-363).
|
|
6535
|
+
Its `fileChanges` map rides on the request, so there is nothing to correlate and
|
|
6536
|
+
nothing is re-rendered: the change is classified by the paths it names, through
|
|
6537
|
+
the same protected-path rules every other file tool uses, and the registered
|
|
6538
|
+
payload carries those paths, the change verbatim and `content_sha256` over the
|
|
6539
|
+
map as it arrived. A path that is absolute, or that resolves outside the
|
|
6540
|
+
directory the server named, is refused `hook-io`; a request with a map and no
|
|
6541
|
+
directory (`cwd` or `grantRoot`) is `bridge-request-unbound`. The item-based
|
|
6542
|
+
`item/fileChange/requestApproval`, which carries an identifier and no content,
|
|
6543
|
+
stays declined.
|
|
6544
|
+
|
|
6545
|
+
The thread is started with `approvalPolicy: untrusted` and `sandbox: read-only`.
|
|
6546
|
+
`untrusted` is the wire spelling of the source's `UnlessTrusted`, the only
|
|
6547
|
+
variant under which every command asks, and the server refuses the source name.
|
|
6548
|
+
There is no flag for it: a session gating an unknown fraction of itself is what
|
|
6549
|
+
the pin exists to prevent (APRV-366).
|
|
6550
|
+
|
|
6551
|
+
The pin is checked as well as sent, and a failure ends the run rather than
|
|
6552
|
+
declining one request:
|
|
6553
|
+
|
|
6554
|
+
```
|
|
6555
|
+
bridge-thread-start-refused the server refused thread/start, so no thread
|
|
6556
|
+
exists and no policy was established; its own
|
|
6557
|
+
error is carried verbatim
|
|
6558
|
+
bridge-approval-policy-mismatch the server reported an effective approval
|
|
6559
|
+
policy that is not untrusted
|
|
6560
|
+
```
|
|
6561
|
+
|
|
6562
|
+
A server that reports no policy at all is run against, because the observed
|
|
6563
|
+
0.155.0 server echoes none and a client demanding an echo could not start. The
|
|
6564
|
+
report says which case it was: `thread.requested` is what went on the wire,
|
|
6565
|
+
`thread.effective` is what the server said, and `thread.confirmed` names where
|
|
6566
|
+
the claim comes from: `unconfirmed`, `reported` (a frame echoed the pin back)
|
|
6567
|
+
or `observed` (a probe command produced an approval request that reached this
|
|
6568
|
+
client). Both stops exit 4, as every other protocol stop in this verb does; the
|
|
6569
|
+
code in the report is the part to branch on.
|
|
6570
|
+
|
|
6571
|
+
**A preflight probe runs before every turn** (APRV-364). Codex's auto-reviewer
|
|
6572
|
+
can resolve an approval with a model call before this client is asked, and
|
|
6573
|
+
nothing in the protocol reports whether it is running, so the bridge watches one
|
|
6574
|
+
command instead of reading a setting. Each start asks a preflight turn for
|
|
6575
|
+
`true` and nothing else, before the operator's prompt, with no flag to skip it;
|
|
6576
|
+
it costs one turn per start. The probe's own request is declined immediately as
|
|
6577
|
+
an observation and never reaches the gate, so nothing is registered for it and
|
|
6578
|
+
no approver is asked about it.
|
|
6579
|
+
|
|
6580
|
+
```
|
|
6581
|
+
bridge-preflight-void the preflight turn ran no command at all, so
|
|
6582
|
+
nothing was established; the report carries
|
|
6583
|
+
the turn's frames verbatim and nothing is
|
|
6584
|
+
retried. Run the verb again
|
|
6585
|
+
bridge-auto-reviewer-active an item/autoApprovalReview notification
|
|
6586
|
+
arrived in either turn: something other than
|
|
6587
|
+
this client answered a question
|
|
6588
|
+
```
|
|
6589
|
+
|
|
6590
|
+
A probe that RAN without asking is `bridge-approval-policy-mismatch`, because a
|
|
6591
|
+
policy under which one command did not ask is not `untrusted` whatever the
|
|
6592
|
+
server reports about itself. A probe that was asked about lets the real turn
|
|
6593
|
+
run, and `thread.confirmed` becomes `observed`. That word is narrow on purpose:
|
|
6594
|
+
it says one question reached this client unanswered by anything else, and it is
|
|
6595
|
+
not a claim that the auto-reviewer is off. The report's `preflight` block
|
|
6596
|
+
carries the turn id, the command, the outcome (`asked`, `executed`, `void` or
|
|
6597
|
+
`pending`), the word sent, and, on a void, the frames.
|
|
6598
|
+
|
|
6599
|
+
**It is an advisory checkpoint and not a boundary**, for reasons
|
|
6600
|
+
docs/codex-app-server-bridge.md states in full: Codex's auto-reviewer can
|
|
6601
|
+
resolve a question before this client sees it, and the approval policy and
|
|
6602
|
+
sandbox posture decide how many questions exist. An open gate window is not
|
|
6603
|
+
honoured here either, which is the strict direction. The claim it supports is
|
|
6604
|
+
"this client decided every question this app-server child asked in this
|
|
6605
|
+
session", and nothing wider.
|
|
6606
|
+
|
|
6607
|
+
**Custody is the operating system's** (APRV-365). The server is started by this
|
|
6608
|
+
verb as its own child over stdio pipes: there is no socket, nothing binds a
|
|
6609
|
+
path, and no other process holds a descriptor to speak on, so the replay of a
|
|
6610
|
+
pending request to whatever connects next cannot arise inside one run. The
|
|
6611
|
+
scope of the claim is that child and that session; a Codex started outside this
|
|
6612
|
+
arrangement is a different process and nothing here observes it. The verb does
|
|
6613
|
+
not inspect the command after `--` for a shape that would attach to something
|
|
6614
|
+
already running instead: that would be a guess at another program's command
|
|
6615
|
+
line, and a check written against a guessed shape finds nothing while reporting
|
|
6616
|
+
that it looked.
|