approval-md 0.1.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 +629 -559
- package/SPEC.md +99 -24
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +656 -0
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1944 -0
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +350 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +50 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +879 -0
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +80 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- 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 +2 -0
- package/dist/src/cli/codex.js +469 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +586 -17
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +107 -0
- package/dist/src/cli/help.js +320 -93
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +126 -0
- package/dist/src/cli/hook-codex.js +226 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +787 -0
- package/dist/src/cli/hook.js +1235 -181
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +159 -7
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy-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.d.ts +43 -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 +501 -0
- package/dist/src/cli/preflight.js +689 -45
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +126 -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-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +277 -0
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +204 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +119 -53
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +344 -11
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/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/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/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/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-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/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +221 -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 +278 -0
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +635 -0
- package/dist/src/core/attest.js +326 -4
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +510 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/budgets.d.ts +238 -0
- 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/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +697 -0
- package/dist/src/core/command-class.js +713 -25
- 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/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/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 +432 -0
- 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 +206 -0
- 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 +455 -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 +871 -0
- package/dist/src/core/execute.js +59 -8
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1449 -0
- package/dist/src/core/gate.js +149 -14
- 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/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +4 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +310 -0
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +316 -0
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +160 -0
- package/dist/src/core/policy-explain.js +63 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +567 -0
- package/dist/src/core/policy-load.js +36 -6
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +324 -0
- package/dist/src/core/policy-match.js +72 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +317 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +566 -0
- package/dist/src/core/protected-path-guard.js +848 -55
- 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/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +371 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/seal.d.ts +165 -0
- 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/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +147 -0
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +476 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +17 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +1316 -63
- package/docs/codex-enforced-session.md +103 -0
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +14 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +539 -9
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- 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 +75 -3
- package/schema/values.schema.json +7 -11
- package/templates/codex/README.md +9 -0
- package/schema/fixtures/values/invalid/version-string.json +0 -1
|
@@ -32,7 +32,23 @@
|
|
|
32
32
|
* Self-reported text is never read. The hook passes the command only; the
|
|
33
33
|
* harness's `description` field is authored by the very agent being gated
|
|
34
34
|
* (SPEC.md §11.1: self-reported fields never reduce scrutiny).
|
|
35
|
+
*
|
|
36
|
+
* Two imports, and both are pure in exactly the same way this file is: no disk,
|
|
37
|
+
* no clock, no environment, no dependencies.
|
|
38
|
+
*
|
|
39
|
+
* `core/read-scope.ts` (APRV-347) holds the read-side path arithmetic, so the
|
|
40
|
+
* hook and the policy explainer can ask the same questions this file asks, of
|
|
41
|
+
* the same table, rather than each growing a copy of it.
|
|
42
|
+
*
|
|
43
|
+
* `core/policy-match.ts` (APRV-354) is imported for ONE name, the
|
|
44
|
+
* `harness.launch.` prefix. The classifier emits the family and two enforcement
|
|
45
|
+
* paths refuse a member of it that no policy rule names, so the three have to
|
|
46
|
+
* agree on what the family is; a second spelling of the prefix would close one
|
|
47
|
+
* of those doors and leave the other open. The import is type-safe in the
|
|
48
|
+
* dependency sense as well: `policy-match.ts` itself imports only types.
|
|
35
49
|
*/
|
|
50
|
+
import { HARNESS_LAUNCH_PREFIX } from "./policy-match.js";
|
|
51
|
+
import { READ_OUT_OF_SCOPE_CLASS, isUnreadableTarget, readTargetVerdict, readTargetsOf, } from "./read-scope.js";
|
|
36
52
|
/**
|
|
37
53
|
* The pass-through pseudo-class for the gate's own CLI.
|
|
38
54
|
*
|
|
@@ -240,6 +256,46 @@ export function protectedPathClass(candidate, extra = []) {
|
|
|
240
256
|
if (next === "hooks.json" || next === "hooks" || next === "agents")
|
|
241
257
|
return "policy.core";
|
|
242
258
|
}
|
|
259
|
+
// Grok Build's equivalent: `.grok/hooks/*.json` is where its PreToolUse
|
|
260
|
+
// entries are installed, and the scripts beside them are what those
|
|
261
|
+
// entries run. Same property as `.cursor/hooks.json` above and for the
|
|
262
|
+
// same reason (APRV-243): an agent that could write those could write
|
|
263
|
+
// itself out of the gate. Grok also READS `.claude/settings.json` and
|
|
264
|
+
// `.cursor/hooks.json` for compatibility, and both are already here.
|
|
265
|
+
if (segment === ".grok") {
|
|
266
|
+
const next = segments[index + 1];
|
|
267
|
+
if (next === "hooks.json" || next === "hooks")
|
|
268
|
+
return "policy.core";
|
|
269
|
+
}
|
|
270
|
+
// Muse Code's equivalent, OBSERVED rather than guessed (APRV-350): the live
|
|
271
|
+
// probe on muse-bin-1.3.0-R3233.1 established that the installed build reads
|
|
272
|
+
// `.muse/hooks.json` in the project, and that it rejects a malformed one
|
|
273
|
+
// loudly at startup. `settings` is listed beside it because Meta documents
|
|
274
|
+
// user-level hooks inside a `settings.json` `hooks` block, so a
|
|
275
|
+
// project-level copy would be the same organ under a second name; it never
|
|
276
|
+
// fired in the probe, and an entry for a file Muse does not read is INERT,
|
|
277
|
+
// while a missing entry for one it does read would be the hole.
|
|
278
|
+
//
|
|
279
|
+
// `worktrees` is deliberately NOT here. Muse keeps its own worktree state
|
|
280
|
+
// under `.muse/worktrees/`, which is ordinary workspace content: making it
|
|
281
|
+
// `policy.core` would price routine session bookkeeping at a human's
|
|
282
|
+
// attention, which is the failure mode §11 asks to avoid.
|
|
283
|
+
if (segment === ".muse") {
|
|
284
|
+
const next = segments[index + 1];
|
|
285
|
+
if (next === "hooks.json" || next === "hooks" || next?.startsWith("settings")) {
|
|
286
|
+
return "policy.core";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Codex installs its hook through these configuration and script paths.
|
|
290
|
+
if (segment === ".codex") {
|
|
291
|
+
const next = segments[index + 1];
|
|
292
|
+
if (next === undefined ||
|
|
293
|
+
next === "config.toml" ||
|
|
294
|
+
next === "hooks.json" ||
|
|
295
|
+
next === "hooks" ||
|
|
296
|
+
segments.slice(index + 1).includes(".."))
|
|
297
|
+
return "policy.core";
|
|
298
|
+
}
|
|
243
299
|
}
|
|
244
300
|
// 3. The policy's own routed entries (APRV-266), above the built-in
|
|
245
301
|
// `policy.edit` set so a routing can re-label one of those paths, and
|
|
@@ -462,6 +518,19 @@ export const NON_SECRET_ENV_NAMES = [
|
|
|
462
518
|
"APPROVAL_MD",
|
|
463
519
|
"APPROVAL_HOME",
|
|
464
520
|
"APPROVAL_DIR",
|
|
521
|
+
/**
|
|
522
|
+
* Where the per-machine bot-ownership registry lives (APRV-390).
|
|
523
|
+
*
|
|
524
|
+
* A DIRECTORY PATH, and the same kind of thing `APPROVAL_HOME` and
|
|
525
|
+
* `APPROVAL_DIR` already are. It holds no secret and opens nothing: the file
|
|
526
|
+
* it points at carries bot ids, usernames and instance directories, all of
|
|
527
|
+
* which `.approval/env` carries in the open, and nothing reads it to widen a
|
|
528
|
+
* permission. Passed through because a child `approval` verb must resolve the
|
|
529
|
+
* SAME registry as its parent — a child that silently fell back to the
|
|
530
|
+
* platform default would answer "which instance owns this bot?" from a
|
|
531
|
+
* different file than the process that asked it.
|
|
532
|
+
*/
|
|
533
|
+
"APPROVAL_STATE_DIR",
|
|
465
534
|
];
|
|
466
535
|
/**
|
|
467
536
|
* Does this bare variable name name credential material?
|
|
@@ -542,6 +611,33 @@ const OPERATOR_CHARS = new Set(["&", "|", ";", "(", ")", "<", ">", "\n"]);
|
|
|
542
611
|
* Not understood, on purpose: parameter expansion values. `$VAR` and `${VAR}`
|
|
543
612
|
* are kept verbatim in the word text, and every rule that reads a path or a
|
|
544
613
|
* refspec treats a word containing `$` as unknown, which resolves stricter.
|
|
614
|
+
*
|
|
615
|
+
* ## Quoted text is DATA, and that is a contract (APRV-353)
|
|
616
|
+
*
|
|
617
|
+
* The command boundary this tokenizer honours is the shell's own. A quoted
|
|
618
|
+
* argument is ONE word to the shell, so nothing inside it is an operator, a
|
|
619
|
+
* redirection, a segment separator or a command name here either:
|
|
620
|
+
*
|
|
621
|
+
* - single-quoted text is wholly inert, every byte of it, `$` and backtick
|
|
622
|
+
* included;
|
|
623
|
+
* - double-quoted text is inert too, with the two exceptions the shell itself
|
|
624
|
+
* makes — `$(…)` and backticks, which it expands before the command runs and
|
|
625
|
+
* which therefore keep the class they have anywhere else (a substitution is
|
|
626
|
+
* classified recursively, a backtick is `opaque`);
|
|
627
|
+
* - adjacent quoted and unquoted runs concatenate into one word (`'a'"b"c`),
|
|
628
|
+
* and a backslash escape is applied where the shell applies it;
|
|
629
|
+
* - UNQUOTED operators split exactly as they always did, and quoting that does
|
|
630
|
+
* not balance is {@link LexResult} `ok: false` — `unparseable`, a refusal —
|
|
631
|
+
* rather than a guess at what the writer meant.
|
|
632
|
+
*
|
|
633
|
+
* This is stated rather than merely true because the failure it prevents is
|
|
634
|
+
* silent and one-directional. A backlog note that says the word `bash`, carries
|
|
635
|
+
* an angle-bracketed placeholder, a pipe or a semicolon is prose about work; a
|
|
636
|
+
* tokenizer that read it as syntax would refuse an ordinary workspace write and
|
|
637
|
+
* push its author toward rewording the record of what they did, which is the
|
|
638
|
+
* audit cost SPEC.md §11 exists to protect. Every printable ASCII character is
|
|
639
|
+
* covered in both quote styles by `tests/command-class-quoting.test.ts`, so an
|
|
640
|
+
* edit that loses the property fails there rather than in someone's notes.
|
|
545
641
|
*/
|
|
546
642
|
function lex(command) {
|
|
547
643
|
const segments = [];
|
|
@@ -727,6 +823,19 @@ function lex(command) {
|
|
|
727
823
|
flush(command.length);
|
|
728
824
|
return { ok: true, segments };
|
|
729
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* The refusal detail for a backtick the shell would expand inside a
|
|
828
|
+
* double-quoted argument (APRV-353).
|
|
829
|
+
*
|
|
830
|
+
* Same code (`opaque`), same verdict (deny), more use: double quotes are what
|
|
831
|
+
* an author reaches for when the text carries an apostrophe, and a note that
|
|
832
|
+
* quotes a command in backticks is then legal shell that really does run
|
|
833
|
+
* something. The refusal names the spelling that is inert, because a refusal a
|
|
834
|
+
* reader cannot act on costs the same attention as one they can (SPEC.md §11.1:
|
|
835
|
+
* refusals are machine-readable and distinct — the code stays the machine's
|
|
836
|
+
* half, this is the human's).
|
|
837
|
+
*/
|
|
838
|
+
const QUOTED_BACKTICK_OPAQUE = "backtick command substitution inside a double-quoted argument, which the shell expands; single quotes make the same text literal";
|
|
730
839
|
/** Scan a double-quoted string starting at the opening quote. */
|
|
731
840
|
function readDoubleQuoted(command, start) {
|
|
732
841
|
let text = "";
|
|
@@ -750,7 +859,7 @@ function readDoubleQuoted(command, start) {
|
|
|
750
859
|
const close = command.indexOf("`", index + 1);
|
|
751
860
|
if (close === -1)
|
|
752
861
|
return null;
|
|
753
|
-
opaque =
|
|
862
|
+
opaque = QUOTED_BACKTICK_OPAQUE;
|
|
754
863
|
index = close;
|
|
755
864
|
continue;
|
|
756
865
|
}
|
|
@@ -868,35 +977,111 @@ function hasShortFlag(args, letters) {
|
|
|
868
977
|
function isUnknownValue(word) {
|
|
869
978
|
return word.includes("$") || word.includes("*") || word.includes("?") || word.startsWith("~");
|
|
870
979
|
}
|
|
871
|
-
/**
|
|
980
|
+
/** A bare release tag: the conventional `v` plus a semantic-version-shaped value. */
|
|
981
|
+
const V_PREFIXED_SEMVER = /^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
982
|
+
/** Does one side of a push refspec explicitly name a tag? */
|
|
983
|
+
function isTagRef(name) {
|
|
984
|
+
if (name.startsWith("refs/tags/"))
|
|
985
|
+
return true;
|
|
986
|
+
if (name.startsWith("refs/heads/"))
|
|
987
|
+
return false;
|
|
988
|
+
return V_PREFIXED_SEMVER.test(name);
|
|
989
|
+
}
|
|
990
|
+
/** Does either source or destination of this refspec explicitly name a tag? */
|
|
991
|
+
function isTagRefspec(refspec) {
|
|
992
|
+
const colon = refspec.indexOf(":");
|
|
993
|
+
if (colon === -1)
|
|
994
|
+
return isTagRef(refspec);
|
|
995
|
+
return isTagRef(refspec.slice(0, colon)) || isTagRef(refspec.slice(colon + 1));
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Deleting a remote ref: its own class, never the trunk-push one (APRV-352).
|
|
999
|
+
*
|
|
1000
|
+
* A `git push` that deletes refs was `vcs.push.main` until now, which reads as
|
|
1001
|
+
* "this reaches the trunk" and in a repository that samples trunk pushes
|
|
1002
|
+
* retrospectively means an irreversible removal proceeds unasked and is looked
|
|
1003
|
+
* at afterwards. It is not the same act. A push adds commits somebody can still
|
|
1004
|
+
* see; a deletion removes the only name an unmerged branch had, and the
|
|
1005
|
+
* reflog that could find it again lives on a server nobody in the session can
|
|
1006
|
+
* reach. The two belong on separate policy lines, and a policy that wants them
|
|
1007
|
+
* on one can still write `vcs.*`.
|
|
1008
|
+
*
|
|
1009
|
+
* Distinct from `vcs.history.rewrite` as well, which guards SHARED history: a
|
|
1010
|
+
* force push moves a ref other people have already built on. That class stays
|
|
1011
|
+
* exactly where it was, above this one, so a force push that also deletes is
|
|
1012
|
+
* still a rewrite.
|
|
1013
|
+
*/
|
|
1014
|
+
const REF_DELETE_CLASS = "vcs.ref.delete";
|
|
1015
|
+
/**
|
|
1016
|
+
* The ref a deleting refspec names, or `null` when the refspec deletes nothing.
|
|
1017
|
+
*
|
|
1018
|
+
* `:dst` (empty source) is the deletion git documents; `src:` (empty
|
|
1019
|
+
* destination) is the spelling the classifier has always treated as one too,
|
|
1020
|
+
* and it keeps doing so rather than being narrowed here. The non-empty side is
|
|
1021
|
+
* the name worth showing an approver either way.
|
|
1022
|
+
*/
|
|
1023
|
+
function deletedRef(refspec) {
|
|
1024
|
+
const colon = refspec.indexOf(":");
|
|
1025
|
+
if (colon === -1)
|
|
1026
|
+
return null;
|
|
1027
|
+
const source = refspec.slice(0, colon);
|
|
1028
|
+
const destination = refspec.slice(colon + 1);
|
|
1029
|
+
if (destination.length === 0)
|
|
1030
|
+
return source.length === 0 ? refspec : source;
|
|
1031
|
+
if (source.length === 0)
|
|
1032
|
+
return destination;
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
/** `git push` — force, release, deletion, trunk and branch turn on flags and refspecs. */
|
|
872
1036
|
function refineGitPush(ctx) {
|
|
873
1037
|
const args = ctx.args.slice(1);
|
|
874
|
-
if (hasFlag(args, ["--force", "-f", "--force-with-lease", "--force-if-includes"])) {
|
|
1038
|
+
if (hasFlag(args, ["--force", "-f", "--force-with-lease", "--force-if-includes", "--mirror"])) {
|
|
875
1039
|
return { class: "vcs.history.rewrite", rule: "git-push-force" };
|
|
876
1040
|
}
|
|
877
1041
|
const positionals = args.filter((arg) => !isFlag(arg));
|
|
878
|
-
|
|
879
|
-
|
|
1042
|
+
const refspecs = positionals.slice(1);
|
|
1043
|
+
if (refspecs.some((refspec) => refspec.startsWith("+"))) {
|
|
1044
|
+
return { class: "vcs.history.rewrite", rule: "git-push-force" };
|
|
1045
|
+
}
|
|
1046
|
+
// The tag check stays ABOVE the deletion check, deliberately. A tag is the
|
|
1047
|
+
// name a release was published under, and this repository's policy prices
|
|
1048
|
+
// `release.publish` accordingly; deleting one is a release act whichever
|
|
1049
|
+
// spelling removes it. Moving the deletion check up would re-label
|
|
1050
|
+
// `git push origin :refs/tags/v1.2.3` and a bulk form that mixes a tag in,
|
|
1051
|
+
// and APRV-352 asks for a class for branch deletions, not a loosening of the
|
|
1052
|
+
// tag surface.
|
|
1053
|
+
if (hasFlag(args, ["--tags", "--follow-tags"]) ||
|
|
1054
|
+
refspecs.some(isTagRefspec) ||
|
|
1055
|
+
refspecs.some((word, index) => word === "tag" && index + 1 < refspecs.length)) {
|
|
1056
|
+
return { class: "release.publish", rule: "git-push-tag" };
|
|
1057
|
+
}
|
|
1058
|
+
// `--delete` / `-d`: every refspec after the remote is a ref being removed.
|
|
1059
|
+
// A `--delete` naming no ref at all is a git error, and it stays in this
|
|
1060
|
+
// class with nothing bound rather than falling through to a push class: an
|
|
1061
|
+
// invocation whose targets cannot be read is the one that least deserves the
|
|
1062
|
+
// looser answer.
|
|
880
1063
|
if (hasFlag(args, ["--delete", "-d"])) {
|
|
881
|
-
return {
|
|
1064
|
+
return {
|
|
1065
|
+
class: REF_DELETE_CLASS,
|
|
1066
|
+
rule: "git-ref-delete",
|
|
1067
|
+
...(refspecs.length === 0 ? {} : { path: refspecs.join(" ") }),
|
|
1068
|
+
};
|
|
882
1069
|
}
|
|
883
|
-
const refspecs = positionals.slice(1);
|
|
884
1070
|
if (refspecs.length === 0) {
|
|
885
1071
|
return { class: "vcs.push.main", rule: "git-push-implicit" };
|
|
886
1072
|
}
|
|
1073
|
+
// The colon-refspec spellings, which need no flag: `:refs/heads/x`, `:x`, and
|
|
1074
|
+
// a bulk form mixing several. ONE deleting refspec makes the whole command a
|
|
1075
|
+
// deletion, because the command's effect is the union of its refspecs and the
|
|
1076
|
+
// destructive half is the half a person is being asked about.
|
|
1077
|
+
const deleted = refspecs.map(deletedRef).filter((ref) => ref !== null);
|
|
1078
|
+
if (deleted.length > 0) {
|
|
1079
|
+
return { class: REF_DELETE_CLASS, rule: "git-ref-delete", path: deleted.join(" ") };
|
|
1080
|
+
}
|
|
887
1081
|
let sawMain = false;
|
|
888
1082
|
for (const refspec of refspecs) {
|
|
889
|
-
if (refspec.startsWith("+")) {
|
|
890
|
-
return { class: "vcs.history.rewrite", rule: "git-push-force" };
|
|
891
|
-
}
|
|
892
1083
|
const colon = refspec.indexOf(":");
|
|
893
1084
|
const destination = colon === -1 ? refspec : refspec.slice(colon + 1);
|
|
894
|
-
// `:branch` (empty source) and `src:` (empty destination) both delete a
|
|
895
|
-
// remote ref. A deletion is destructive whatever it names, so it takes the
|
|
896
|
-
// stricter class rather than the branch one.
|
|
897
|
-
if (destination.length === 0 || (colon !== -1 && refspec.slice(0, colon).length === 0)) {
|
|
898
|
-
return { class: "vcs.push.main", rule: "git-push-delete" };
|
|
899
|
-
}
|
|
900
1085
|
if (isUnknownValue(destination)) {
|
|
901
1086
|
sawMain = true;
|
|
902
1087
|
continue;
|
|
@@ -923,6 +1108,211 @@ function refineGitPush(ctx) {
|
|
|
923
1108
|
* Everything that is not provably scratch keeps the old class.
|
|
924
1109
|
*/
|
|
925
1110
|
const SCRATCH_DELETE_CLASS = "files.delete.scratch";
|
|
1111
|
+
// ---------------------------------------------------------------------------
|
|
1112
|
+
// Agent harnesses (APRV-354)
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
/**
|
|
1115
|
+
* Launching an agent harness is its own class family, `harness.launch.NAME`.
|
|
1116
|
+
*
|
|
1117
|
+
* Until this, a command whose first word was `codex`, `muse`, `grok`, `claude`
|
|
1118
|
+
* or `cursor-agent` was `unclassified`: denied, which is fail closed and also
|
|
1119
|
+
* blunt. It told an approver nothing, it gave a human no class to grant through
|
|
1120
|
+
* the ordinary manual path, and it meant a lane could not so much as read a
|
|
1121
|
+
* harness version without going around the gate. The family fixes the second
|
|
1122
|
+
* half without touching the first: an unknown class still falls to
|
|
1123
|
+
* `defaults.autonomy`, so a project that adopts nothing is exactly where it was.
|
|
1124
|
+
*
|
|
1125
|
+
* ## The laundering hazard, which is why this is a class and not a table row
|
|
1126
|
+
*
|
|
1127
|
+
* A launched harness runs its own tools OUTSIDE this gate unless that harness's
|
|
1128
|
+
* own adapter is installed and attested. So a grant of `harness.launch.codex`
|
|
1129
|
+
* authorizes the LAUNCH and nothing the launched session then does — every
|
|
1130
|
+
* file it writes, every command it runs and every request it makes happens
|
|
1131
|
+
* where this runtime cannot see it. A policy that made the family autonomous
|
|
1132
|
+
* would have written itself out of the gate in one line, which is why SPEC.md
|
|
1133
|
+
* §7 says the family is never inferred autonomous and why the reference
|
|
1134
|
+
* proposal holds it at `manual`. The blessed Codex entry point is the confined
|
|
1135
|
+
* `approval codex start` (APRV-325.3), which keeps its own class.
|
|
1136
|
+
*
|
|
1137
|
+
* ## Muse, which carries a second hazard
|
|
1138
|
+
*
|
|
1139
|
+
* A Muse Code model id ending `-contributor` trades price for permission to
|
|
1140
|
+
* train on prompts and completions, so running one over real code shares that
|
|
1141
|
+
* code. Carter has ruled that Muse must never run with a Contributor model
|
|
1142
|
+
* selected. The classifier cannot enforce that — the model is a self-reported
|
|
1143
|
+
* argument, and a session may select one in its own UI where no command line
|
|
1144
|
+
* shows it — so it does the one thing a classifier honestly can: it BINDS the
|
|
1145
|
+
* `--model` value where it appears and gives a contributor value its own rule
|
|
1146
|
+
* id, so a prompt and a policy can both see it. This may only ever RAISE
|
|
1147
|
+
* scrutiny. A `-standard` id, or no `--model` at all, returns the ordinary rule
|
|
1148
|
+
* id and changes nothing, because a self-reported field that could lower
|
|
1149
|
+
* scrutiny is a field an agent would learn to write (SPEC.md §11.1 invariant 4).
|
|
1150
|
+
*
|
|
1151
|
+
* ## Where the family's name lives
|
|
1152
|
+
*
|
|
1153
|
+
* {@link HARNESS_LAUNCH_PREFIX} is `core/policy-match.ts`'s, not this file's,
|
|
1154
|
+
* and is imported rather than repeated. Two enforcement paths refuse a launch
|
|
1155
|
+
* that no policy rule names (`harnessLaunchNeedsRule`), and a second spelling of
|
|
1156
|
+
* the prefix is the shape of bug that closes one of those doors and leaves the
|
|
1157
|
+
* other open.
|
|
1158
|
+
*/
|
|
1159
|
+
/** The rule id prefix, so a reader can tell a launch row from a probe. */
|
|
1160
|
+
const HARNESS_LAUNCH_RULE_PREFIX = "harness-launch-";
|
|
1161
|
+
/**
|
|
1162
|
+
* Harness binaries, by BASENAME, to the name their class carries.
|
|
1163
|
+
*
|
|
1164
|
+
* Basenames, because that is what {@link classifySegment} derives before any
|
|
1165
|
+
* rule sees a command, and it is what makes `/opt/homebrew/bin/codex`,
|
|
1166
|
+
* `~/.local/bin/muse` and `$HOME/.local/bin/muse` all land here without this
|
|
1167
|
+
* table knowing anything about where a binary lives. A spelling the basename
|
|
1168
|
+
* derivation cannot see through — `$MUSE_BIN`, a wrapper script of another
|
|
1169
|
+
* name — is `unclassified`, which is the answer it had before and the answer it
|
|
1170
|
+
* should keep.
|
|
1171
|
+
*
|
|
1172
|
+
* `cursor-agent` carries the name `cursor` so the class reads
|
|
1173
|
+
* `harness.launch.cursor` beside the `cursor` adapter and the `.cursor/`
|
|
1174
|
+
* protected paths. `gemini` is deliberately absent: APRV-354 names five
|
|
1175
|
+
* harnesses, `gemini update` keeps its `deps.upgrade` row above this one, and a
|
|
1176
|
+
* bare `gemini` stays `unclassified` until somebody makes that its own decision.
|
|
1177
|
+
*/
|
|
1178
|
+
const HARNESS_BINS = {
|
|
1179
|
+
codex: "codex",
|
|
1180
|
+
muse: "muse",
|
|
1181
|
+
grok: "grok",
|
|
1182
|
+
claude: "claude",
|
|
1183
|
+
"cursor-agent": "cursor",
|
|
1184
|
+
};
|
|
1185
|
+
/**
|
|
1186
|
+
* Package specs a package runner may name, EXACTLY, to the same harness names.
|
|
1187
|
+
*
|
|
1188
|
+
* Exact, and the exactness is the rule: `npx codex-helper` is not a codex
|
|
1189
|
+
* launch, and a substring match that said it was would let any package whose
|
|
1190
|
+
* name happens to contain a harness's take a class it did not earn. A spec this
|
|
1191
|
+
* table does not know keeps whatever class the runner already had.
|
|
1192
|
+
*/
|
|
1193
|
+
const HARNESS_PACKAGES = {
|
|
1194
|
+
codex: "codex",
|
|
1195
|
+
"@openai/codex": "codex",
|
|
1196
|
+
claude: "claude",
|
|
1197
|
+
"@anthropic-ai/claude-code": "claude",
|
|
1198
|
+
"cursor-agent": "cursor",
|
|
1199
|
+
muse: "muse",
|
|
1200
|
+
grok: "grok",
|
|
1201
|
+
};
|
|
1202
|
+
/**
|
|
1203
|
+
* Argv that starts nothing: a version or help probe.
|
|
1204
|
+
*
|
|
1205
|
+
* A probe prints a string and exits, so it is a read, and reading a harness's
|
|
1206
|
+
* own version is exactly what a session needs to be able to do without a
|
|
1207
|
+
* prompt. `help` counts only as the WHOLE argv: `codex help` prints usage,
|
|
1208
|
+
* while `codex help me refactor this` is a session.
|
|
1209
|
+
*/
|
|
1210
|
+
const HARNESS_PROBE_FLAGS = ["--version", "-V", "--help", "-h"];
|
|
1211
|
+
/** The probe's rule id and class. A probe starts no session, so it reads. */
|
|
1212
|
+
const HARNESS_PROBE_RULE = "harness-probe";
|
|
1213
|
+
const HARNESS_PROBE_CLASS = "read.shell";
|
|
1214
|
+
/** The rule id a Muse launch takes when its `--model` names a Contributor model. */
|
|
1215
|
+
const MUSE_CONTRIBUTOR_RULE = "harness-launch-muse-contributor";
|
|
1216
|
+
/**
|
|
1217
|
+
* The suffix that marks a Muse model as training on what it is shown.
|
|
1218
|
+
*
|
|
1219
|
+
* Exported since APRV-350 so the hook adapter's contributor guard and this
|
|
1220
|
+
* classifier's `harness.launch.muse` refinement test the SAME mark. Two
|
|
1221
|
+
* spellings of "which models are unsafe" would drift, and the direction they
|
|
1222
|
+
* drift in is the one where a launch is refused and a tool call is not.
|
|
1223
|
+
*/
|
|
1224
|
+
export const CONTRIBUTOR_SUFFIX = "-contributor";
|
|
1225
|
+
/** The `--model` value in either spelling, or `null` when none is written. */
|
|
1226
|
+
function harnessModel(args) {
|
|
1227
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1228
|
+
const arg = args[index];
|
|
1229
|
+
if (arg === "--model") {
|
|
1230
|
+
const value = args[index + 1];
|
|
1231
|
+
return value === undefined || isFlag(value) ? null : value;
|
|
1232
|
+
}
|
|
1233
|
+
if (arg.startsWith("--model="))
|
|
1234
|
+
return arg.slice("--model=".length);
|
|
1235
|
+
}
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* The harness a package spec names, version suffix stripped, or `null`.
|
|
1240
|
+
*
|
|
1241
|
+
* `@openai/codex@0.152.1` is the scoped case the split has to get right: the
|
|
1242
|
+
* `@` that opens a scope is not the `@` that opens a version.
|
|
1243
|
+
*/
|
|
1244
|
+
function harnessPackage(spec) {
|
|
1245
|
+
const at = spec.startsWith("@") ? spec.indexOf("@", 1) : spec.indexOf("@");
|
|
1246
|
+
const bare = at === -1 ? spec : spec.slice(0, at);
|
|
1247
|
+
return HARNESS_PACKAGES[bare] ?? null;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* One harness invocation, given its name and the argv that follows its identity.
|
|
1251
|
+
*
|
|
1252
|
+
* Shared by the direct rows and the package-runner refinement, so a launch
|
|
1253
|
+
* spelled `npx @openai/codex exec` answers exactly as `codex exec` does. The
|
|
1254
|
+
* argv is bound to `path`: the segment's own `text` carries the command as
|
|
1255
|
+
* written, and `path` carries the part the class is ABOUT, which is what lets a
|
|
1256
|
+
* channel name it without re-parsing the line.
|
|
1257
|
+
*/
|
|
1258
|
+
function harnessRefinement(name, args) {
|
|
1259
|
+
if (args.length === 1 &&
|
|
1260
|
+
(HARNESS_PROBE_FLAGS.includes(args[0]) || args[0] === "help")) {
|
|
1261
|
+
return { class: HARNESS_PROBE_CLASS, rule: HARNESS_PROBE_RULE };
|
|
1262
|
+
}
|
|
1263
|
+
// Anything else is a session. A probe flag beside other arguments is NOT a
|
|
1264
|
+
// probe here: the classifier cannot know which of the two the binary will
|
|
1265
|
+
// honour, and the stricter reading of an ambiguous harness invocation is the
|
|
1266
|
+
// one that assumes a session started.
|
|
1267
|
+
const model = name === "muse" ? harnessModel(args) : null;
|
|
1268
|
+
const contributor = model !== null && model.toLowerCase().endsWith(CONTRIBUTOR_SUFFIX);
|
|
1269
|
+
return {
|
|
1270
|
+
class: `${HARNESS_LAUNCH_PREFIX}${name}`,
|
|
1271
|
+
rule: contributor ? MUSE_CONTRIBUTOR_RULE : `${HARNESS_LAUNCH_RULE_PREFIX}${name}`,
|
|
1272
|
+
...(args.length === 0 ? {} : { path: args.join(" ") }),
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
/** `codex …`, `muse …`, `grok …`, `claude …`, `cursor-agent …`. */
|
|
1276
|
+
function refineHarness(ctx) {
|
|
1277
|
+
const name = HARNESS_BINS[ctx.bin];
|
|
1278
|
+
// Unreachable through the table, which matches on these basenames; a defensive
|
|
1279
|
+
// arm rather than a silent wrong class if a row is ever edited apart from the
|
|
1280
|
+
// table it is generated from.
|
|
1281
|
+
if (name === undefined) {
|
|
1282
|
+
return { opaque: `${ctx.bin} is an agent harness this table cannot name` };
|
|
1283
|
+
}
|
|
1284
|
+
return harnessRefinement(name, ctx.args);
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* `npx`, `tsx`, `tsc`, … — unchanged, except a package runner naming a harness.
|
|
1288
|
+
*
|
|
1289
|
+
* `npx @openai/codex` starts the same session `codex` starts, and before this
|
|
1290
|
+
* it was `files.write.workspace`: the looser of the two answers, reachable by
|
|
1291
|
+
* typing four extra characters. The refinement is deliberately narrow — only
|
|
1292
|
+
* `npx`, only an EXACT package spec, and everything else returns the row's own
|
|
1293
|
+
* answer byte for byte, which is what keeps this from being a widening of the
|
|
1294
|
+
* workspace-tool row.
|
|
1295
|
+
*/
|
|
1296
|
+
function refineWorkspaceTool(ctx) {
|
|
1297
|
+
const unchanged = { class: "files.write.workspace", rule: "workspace-tool" };
|
|
1298
|
+
if (ctx.bin !== "npx")
|
|
1299
|
+
return unchanged;
|
|
1300
|
+
const index = ctx.args.findIndex((arg) => !isFlag(arg));
|
|
1301
|
+
if (index === -1)
|
|
1302
|
+
return unchanged;
|
|
1303
|
+
const name = harnessPackage(ctx.args[index]);
|
|
1304
|
+
if (name === null)
|
|
1305
|
+
return unchanged;
|
|
1306
|
+
return harnessRefinement(name, ctx.args.slice(index + 1));
|
|
1307
|
+
}
|
|
1308
|
+
/** The five generated harness rows, one per binary, sharing one refinement. */
|
|
1309
|
+
const HARNESS_RULES = Object.entries(HARNESS_BINS).map(([bin, name]) => ({
|
|
1310
|
+
id: `${HARNESS_LAUNCH_RULE_PREFIX}${name}`,
|
|
1311
|
+
bins: [bin],
|
|
1312
|
+
class: `${HARNESS_LAUNCH_PREFIX}${name}`,
|
|
1313
|
+
emits: [HARNESS_PROBE_CLASS],
|
|
1314
|
+
refine: refineHarness,
|
|
1315
|
+
}));
|
|
926
1316
|
/**
|
|
927
1317
|
* Is `candidate` a STRICT descendant of `root`? Both are compared by path
|
|
928
1318
|
* segment, so `/private/tmpfoo` is not under `/private/tmp` and a root is never
|
|
@@ -966,6 +1356,43 @@ function allTargetsAreScratch(targets, roots) {
|
|
|
966
1356
|
}
|
|
967
1357
|
return true;
|
|
968
1358
|
}
|
|
1359
|
+
// ===========================================================================
|
|
1360
|
+
// Read scope (read.file.out_of_scope, APRV-347)
|
|
1361
|
+
// ===========================================================================
|
|
1362
|
+
/** The rule id for a read whose ABSOLUTE target sits outside every root. */
|
|
1363
|
+
export const READ_OUT_OF_SCOPE_RULE = "read-out-of-scope";
|
|
1364
|
+
/** The rule id for a read target whose expansion the text cannot show. */
|
|
1365
|
+
export const READ_UNREADABLE_TARGET_RULE = "read-unreadable-path";
|
|
1366
|
+
/**
|
|
1367
|
+
* The first target of this read command that the TEXT places outside every
|
|
1368
|
+
* root, or `null` when nothing here settles it.
|
|
1369
|
+
*
|
|
1370
|
+
* `null` covers three different situations on purpose, and all three are
|
|
1371
|
+
* handed on rather than decided: the binary is not a scoped reader, every
|
|
1372
|
+
* target is provably inside a root, or a target is relative (or carries `..`)
|
|
1373
|
+
* and therefore means nothing without a working directory. The last of those
|
|
1374
|
+
* is the common case, and it is why the hook's second pass exists.
|
|
1375
|
+
*/
|
|
1376
|
+
function escapedReadTarget(bin, positionals, args, roots) {
|
|
1377
|
+
const targets = readTargetsOf(bin, positionals, args);
|
|
1378
|
+
if (targets === null)
|
|
1379
|
+
return null;
|
|
1380
|
+
for (const target of targets) {
|
|
1381
|
+
switch (readTargetVerdict(target, roots)) {
|
|
1382
|
+
case "out-of-scope":
|
|
1383
|
+
return {
|
|
1384
|
+
path: target,
|
|
1385
|
+
rule: isUnreadableTarget(target)
|
|
1386
|
+
? READ_UNREADABLE_TARGET_RULE
|
|
1387
|
+
: READ_OUT_OF_SCOPE_RULE,
|
|
1388
|
+
};
|
|
1389
|
+
case "in-scope":
|
|
1390
|
+
case "needs-disk":
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
969
1396
|
/** `rm` — everything outside the workspace, and every unreadable path, is manual. */
|
|
970
1397
|
function refineRm(ctx) {
|
|
971
1398
|
const recursive = hasFlag(ctx.args, ["--recursive"]) || hasShortFlag(ctx.args, ["r", "R"]);
|
|
@@ -1395,12 +1822,31 @@ function isGateEntrypoint(path) {
|
|
|
1395
1822
|
* ritual reached the approver's phone as `policy.edit` over a protected path —
|
|
1396
1823
|
* true, and useless. Classified by name it arrives as what it is.
|
|
1397
1824
|
*
|
|
1398
|
-
* `
|
|
1399
|
-
*
|
|
1825
|
+
* `policy attest --path` (APRV-338) is the newest member and the one that reads
|
|
1826
|
+
* a FLAG, because the flag is what changes the act. Without it the verb attests
|
|
1827
|
+
* the policy file or a gate organ, which are records about the gate's own
|
|
1828
|
+
* configuration; with it the verb ratifies protected TEXT, and that record is
|
|
1829
|
+
* what resolves SPEC.md's pending-sign-off suffix. An agent able to write one
|
|
1830
|
+
* could ratify its own amendments, so it is classified where the rest of the
|
|
1831
|
+
* gate's ceremonies are: `policy.core`, which this repository's policy holds
|
|
1832
|
+
* human-only, so the hook denies it with `hook-class-human-only` before the
|
|
1833
|
+
* verb's own `actor-not-human` refusal is ever reached. It mints no new class
|
|
1834
|
+
* (§11.1 invariant 9): `policy.core` already exists and is already in the row's
|
|
1835
|
+
* `emits`.
|
|
1836
|
+
*
|
|
1837
|
+
* `positionals` is read rather than `args` for the verb words, so a flag between
|
|
1838
|
+
* them cannot hide the verb: `approval --json log sync` is the same invocation.
|
|
1839
|
+
* `args` is read only where a flag is the act, as it is above.
|
|
1400
1840
|
*/
|
|
1401
|
-
function refineApprovalVerb(positionals) {
|
|
1841
|
+
function refineApprovalVerb(positionals, args = []) {
|
|
1402
1842
|
const verb = positionals[0];
|
|
1403
1843
|
const sub = positionals[1];
|
|
1844
|
+
if (verb === "policy" && sub === "attest" && hasFlag(args, ["--path"])) {
|
|
1845
|
+
return { class: "policy.core", rule: "approval-policy-signoff" };
|
|
1846
|
+
}
|
|
1847
|
+
if (verb === "quickstart") {
|
|
1848
|
+
return { class: "policy.core", rule: "approval-quickstart" };
|
|
1849
|
+
}
|
|
1404
1850
|
if (verb === "log") {
|
|
1405
1851
|
if (sub === "sync")
|
|
1406
1852
|
return { class: "log.sync", rule: "approval-log-sync" };
|
|
@@ -1426,6 +1872,22 @@ function refineApprovalVerb(positionals) {
|
|
|
1426
1872
|
return { class: "policy.core", rule: "approval-gate-close" };
|
|
1427
1873
|
return null;
|
|
1428
1874
|
}
|
|
1875
|
+
// APRV-343. `policy apply` WRITES `APPROVAL.md`, which is the one file in this
|
|
1876
|
+
// repository nothing but a human's own hand may change: an agent that could
|
|
1877
|
+
// run it could widen the policy that governs it and then attest the result
|
|
1878
|
+
// through the amendment the verb goes on to run. Classified where the file
|
|
1879
|
+
// already is (`policy.core`, human-only in the reference policy), so the hook
|
|
1880
|
+
// denies it with `hook-class-human-only`, behind the verb's own
|
|
1881
|
+
// `apply-agent-actor` refusal. It mints no new class (SPEC.md §11.1 invariant
|
|
1882
|
+
// 9): `policy.core` already exists and already covers the policy's machinery.
|
|
1883
|
+
//
|
|
1884
|
+
// The other `policy` subcommands stay pass-through. `check` and `test` read,
|
|
1885
|
+
// `attest` and `amend` refuse a non-human actor in code and collect a human's
|
|
1886
|
+
// tap through a channel when an agent runs them, which is the widening
|
|
1887
|
+
// APRV-109 deliberately made — and neither of them writes the policy file.
|
|
1888
|
+
if (verb === "policy" && sub === "apply") {
|
|
1889
|
+
return { class: "policy.core", rule: "approval-policy-apply" };
|
|
1890
|
+
}
|
|
1429
1891
|
// APRV-257. `setup checkpoint` MINTS the key `log checkpoint` signs with, so
|
|
1430
1892
|
// an agent that could run it could mint a key, store it, and vouch for a
|
|
1431
1893
|
// chain it had just written — the mechanism defeated at its source rather
|
|
@@ -1452,7 +1914,7 @@ function refineApprovalVerb(positionals) {
|
|
|
1452
1914
|
* two log verbs keeps the pass-through class and the row's own rule id.
|
|
1453
1915
|
*/
|
|
1454
1916
|
function refineApproval(ctx) {
|
|
1455
|
-
return refineApprovalVerb(ctx.positionals) ?? { class: GATE_SELF_CLASS, rule: "approval" };
|
|
1917
|
+
return (refineApprovalVerb(ctx.positionals, ctx.args) ?? { class: GATE_SELF_CLASS, rule: "approval" });
|
|
1456
1918
|
}
|
|
1457
1919
|
/**
|
|
1458
1920
|
* `node` — an inline script is opaque, the gate's own entry point is
|
|
@@ -1465,7 +1927,7 @@ function refineNode(ctx) {
|
|
|
1465
1927
|
if (script !== undefined && isGateEntrypoint(script)) {
|
|
1466
1928
|
// `node cli.js log sync` is `approval log sync` spelled the long way, and
|
|
1467
1929
|
// it must classify identically or the classification is a spelling test.
|
|
1468
|
-
return (refineApprovalVerb(ctx.positionals.slice(1)) ?? {
|
|
1930
|
+
return (refineApprovalVerb(ctx.positionals.slice(1), ctx.args) ?? {
|
|
1469
1931
|
class: GATE_SELF_CLASS,
|
|
1470
1932
|
rule: "node-approval-cli",
|
|
1471
1933
|
});
|
|
@@ -1489,7 +1951,7 @@ export const COMMAND_RULES = [
|
|
|
1489
1951
|
bins: ["git"],
|
|
1490
1952
|
subs: ["push"],
|
|
1491
1953
|
class: "vcs.push.main",
|
|
1492
|
-
emits: ["vcs.push.branch", "vcs.push.main", "vcs.history.rewrite"],
|
|
1954
|
+
emits: ["vcs.push.branch", "vcs.push.main", "vcs.history.rewrite", "vcs.ref.delete"],
|
|
1493
1955
|
refine: refineGitPush,
|
|
1494
1956
|
},
|
|
1495
1957
|
{
|
|
@@ -1614,8 +2076,20 @@ export const COMMAND_RULES = [
|
|
|
1614
2076
|
// harness unattended. `uca` matches with ANY arguments, `--dry-run` included:
|
|
1615
2077
|
// the classifier reads text, cannot know which flags the script honours, and
|
|
1616
2078
|
// the strictest reading of an updater is that it updates.
|
|
2079
|
+
//
|
|
2080
|
+
// APRV-354 answers the other half of that sentence: the launch rows below now
|
|
2081
|
+
// name what a bare `claude` or `codex …` is. This row stays ABOVE them on
|
|
2082
|
+
// purpose, so `codex update` and `claude update` keep `deps.upgrade` — an
|
|
2083
|
+
// upgrade swaps the binary that hosts the hook, which is the stricter of the
|
|
2084
|
+
// two readings and the class they already had.
|
|
1617
2085
|
{ id: "harness-update", bins: ["claude", "codex", "gemini"], subs: ["update"], class: "deps.upgrade" },
|
|
1618
2086
|
{ id: "harness-updater", bins: ["uca"], class: "deps.upgrade" },
|
|
2087
|
+
// -- agent harness launch (APRV-354) --------------------------------------
|
|
2088
|
+
// Generated from {@link HARNESS_BINS}, one row per binary, all sharing
|
|
2089
|
+
// {@link refineHarness}. Below `harness-update` so an upgrade keeps its
|
|
2090
|
+
// class; above the workspace tools so a package-runner spelling is the only
|
|
2091
|
+
// one that has to be refined rather than matched.
|
|
2092
|
+
...HARNESS_RULES,
|
|
1619
2093
|
// -- workspace tools -----------------------------------------------------
|
|
1620
2094
|
// APRV-193: three of the rules below hand control to code the runtime did not
|
|
1621
2095
|
// author, and they are named in {@link CODE_EXECUTING_RULES}.
|
|
@@ -1637,6 +2111,14 @@ export const COMMAND_RULES = [
|
|
|
1637
2111
|
id: "workspace-tool",
|
|
1638
2112
|
bins: ["npx", "tsx", "ts-node", "tsc", "oxlint", "eslint", "prettier", "vitest", "jest", "backlog", "make"],
|
|
1639
2113
|
class: "files.write.workspace",
|
|
2114
|
+
// APRV-354: only `npx` naming a harness package changes; see
|
|
2115
|
+
// {@link refineWorkspaceTool}, which returns this row's own answer for
|
|
2116
|
+
// every other binary and every other package.
|
|
2117
|
+
emits: [
|
|
2118
|
+
HARNESS_PROBE_CLASS,
|
|
2119
|
+
...Object.values(HARNESS_PACKAGES).map((name) => `${HARNESS_LAUNCH_PREFIX}${name}`),
|
|
2120
|
+
],
|
|
2121
|
+
refine: refineWorkspaceTool,
|
|
1640
2122
|
},
|
|
1641
2123
|
{
|
|
1642
2124
|
id: "workspace-write",
|
|
@@ -1795,9 +2277,17 @@ function refineGh(ctx) {
|
|
|
1795
2277
|
* Binaries whose effect lives in a string this classifier will not interpret.
|
|
1796
2278
|
*
|
|
1797
2279
|
* A second parser for the same text is a second answer waiting to disagree with
|
|
1798
|
-
* the shell's, so these refuse instead. `
|
|
1799
|
-
*
|
|
2280
|
+
* the shell's, so these refuse instead. `eval`, `xargs` and the `-e`
|
|
2281
|
+
* interpreters can express anything at all; `sudo` and `env` re-launch
|
|
1800
2282
|
* something else with different authority.
|
|
2283
|
+
*
|
|
2284
|
+
* ONE NARROW EXCEPTION since APRV-380, and it is a narrowing of this rule
|
|
2285
|
+
* rather than a hole in it: a segment that is EXACTLY a known shell, one
|
|
2286
|
+
* inline-script flag and one script word is classified by the script, through
|
|
2287
|
+
* this same classifier. No second parser is written and the text is not read a
|
|
2288
|
+
* second way. {@link loginShellScript} states the shape and the reasoning, and
|
|
2289
|
+
* everything outside it — including the same shell with a script file, or a
|
|
2290
|
+
* shell nested inside an unwrapped script — is refused here exactly as before.
|
|
1801
2291
|
*/
|
|
1802
2292
|
const OPAQUE_BINS = {
|
|
1803
2293
|
bash: "runs a shell script",
|
|
@@ -1819,6 +2309,93 @@ const OPAQUE_BINS = {
|
|
|
1819
2309
|
timeout: "runs another command under a timer",
|
|
1820
2310
|
time: "runs another command under a timer",
|
|
1821
2311
|
};
|
|
2312
|
+
/**
|
|
2313
|
+
* The shells {@link loginShellScript} will unwrap: the six in
|
|
2314
|
+
* {@link OPAQUE_BINS} that run a script.
|
|
2315
|
+
*
|
|
2316
|
+
* The other opaque binaries stay opaque under every shape. `eval`, `xargs` and
|
|
2317
|
+
* the `-e` interpreters build their text from somewhere this file cannot see;
|
|
2318
|
+
* `sudo`, `doas`, `env`, `nohup`, `exec`, `source`, `.`, `watch`, `timeout` and
|
|
2319
|
+
* `time` re-launch something else, with different authority or under a timer,
|
|
2320
|
+
* and what they re-launch is an argv rather than a script.
|
|
2321
|
+
*/
|
|
2322
|
+
const UNWRAPPABLE_SHELLS = new Set([
|
|
2323
|
+
"bash",
|
|
2324
|
+
"sh",
|
|
2325
|
+
"zsh",
|
|
2326
|
+
"dash",
|
|
2327
|
+
"ksh",
|
|
2328
|
+
"fish",
|
|
2329
|
+
]);
|
|
2330
|
+
/**
|
|
2331
|
+
* The inline-script flags a wrapper may carry: `-c`, with any run of `l` and
|
|
2332
|
+
* `i` before it (APRV-380).
|
|
2333
|
+
*
|
|
2334
|
+
* `-lc` is the shape the 2026-09-18 probe recorded on every Codex exec request.
|
|
2335
|
+
* `c` must be LAST, because that is the letter that takes the next word as its
|
|
2336
|
+
* argument, and a combination where it is not last is one whose reading depends
|
|
2337
|
+
* on the shell's own option parser. That is a shape this rule declines to guess
|
|
2338
|
+
* at, so it stays opaque.
|
|
2339
|
+
*/
|
|
2340
|
+
const INLINE_SCRIPT_FLAG = /^-[li]*c$/u;
|
|
2341
|
+
/**
|
|
2342
|
+
* The script a LOGIN-SHELL WRAPPER runs, when the segment is exactly one
|
|
2343
|
+
* (APRV-380), or `null`.
|
|
2344
|
+
*
|
|
2345
|
+
* ## Why this is not the second parser the table above refuses
|
|
2346
|
+
*
|
|
2347
|
+
* {@link OPAQUE_BINS} states the position this narrows: a second parser for the
|
|
2348
|
+
* same text is a second answer waiting to disagree with the shell's. The hazard
|
|
2349
|
+
* that names is reading shell text a SECOND WAY. This is not that. When the
|
|
2350
|
+
* argv is exactly `[shell, -lc, script]` and nothing else, the script is the
|
|
2351
|
+
* text a Claude Code `Bash` call hands this classifier directly, and what
|
|
2352
|
+
* happens to it here is what happens to that: the same lexer, the same segment
|
|
2353
|
+
* rules, the same table. No new parser is written, and the text is not read a
|
|
2354
|
+
* second way — it is read the first way, by the only reader there is.
|
|
2355
|
+
*
|
|
2356
|
+
* ## The line, and it is exact
|
|
2357
|
+
*
|
|
2358
|
+
* Three words, no more: a known shell, one inline-script flag, one script. Any
|
|
2359
|
+
* of these keeps the wrapper opaque, because each is a shape whose effect
|
|
2360
|
+
* depends on something the argv alone does not say:
|
|
2361
|
+
*
|
|
2362
|
+
* - extra words (a script FILE, `--`, an option this rule does not model);
|
|
2363
|
+
* - an assignment prefix, which changes the environment the script runs in;
|
|
2364
|
+
* - a redirection on the wrapper, which is the outer shell's and not the
|
|
2365
|
+
* script's;
|
|
2366
|
+
* - a substitution in any of the three words, whose effect happens before the
|
|
2367
|
+
* shell even starts;
|
|
2368
|
+
* - a nested shell inside the script, refused by {@link ClassifierContext} when
|
|
2369
|
+
* the recursion runs (the inner classification unwraps nothing).
|
|
2370
|
+
*
|
|
2371
|
+
* The BINDING does not move. `cli/hook.ts` binds the outer command and argv
|
|
2372
|
+
* exactly as APRV-362 built them; what this changes is only which text is
|
|
2373
|
+
* classified, and the classes are additional evidence about bytes that are
|
|
2374
|
+
* bound elsewhere and unchanged.
|
|
2375
|
+
*/
|
|
2376
|
+
export function loginShellScript(segment) {
|
|
2377
|
+
if (segment.opaque !== null)
|
|
2378
|
+
return null;
|
|
2379
|
+
if (segment.redirects.length > 0)
|
|
2380
|
+
return null;
|
|
2381
|
+
if (segment.words.length !== 3)
|
|
2382
|
+
return null;
|
|
2383
|
+
const [shell, flag, script] = segment.words;
|
|
2384
|
+
if (shell.substitutions.length > 0)
|
|
2385
|
+
return null;
|
|
2386
|
+
if (flag.substitutions.length > 0)
|
|
2387
|
+
return null;
|
|
2388
|
+
if (script.substitutions.length > 0)
|
|
2389
|
+
return null;
|
|
2390
|
+
const base = pathSegments(shell.text).slice(-1)[0] ?? shell.text;
|
|
2391
|
+
if (!UNWRAPPABLE_SHELLS.has(base))
|
|
2392
|
+
return null;
|
|
2393
|
+
if (!INLINE_SCRIPT_FLAG.test(flag.text))
|
|
2394
|
+
return null;
|
|
2395
|
+
// An empty script runs nothing, and `classifyCommand` answers `unclassified`
|
|
2396
|
+
// for an empty command. Leaving it wrapped keeps that answer the wrapper's.
|
|
2397
|
+
return script.text.trim().length === 0 ? null : script.text;
|
|
2398
|
+
}
|
|
1822
2399
|
/** Interpreters that are opaque only when handed inline source. */
|
|
1823
2400
|
const INLINE_SOURCE_BINS = {
|
|
1824
2401
|
python: ["-c"],
|
|
@@ -1927,6 +2504,20 @@ export const CODE_EXECUTING_RULES = [
|
|
|
1927
2504
|
"node-script",
|
|
1928
2505
|
/** `npx`, `tsx`, `tsc`, `vitest`, `jest`, `make`, and kin. */
|
|
1929
2506
|
"workspace-tool",
|
|
2507
|
+
/**
|
|
2508
|
+
* Launching an agent harness, and probing one (APRV-354).
|
|
2509
|
+
*
|
|
2510
|
+
* Every spelling is here, the probe included. A launch hands control to a
|
|
2511
|
+
* whole second agent, which is the most complete form of "code the runtime
|
|
2512
|
+
* did not author"; a probe still executes the same binary. The list is
|
|
2513
|
+
* matched against a segment's RULE, so the generated launch ids and the two
|
|
2514
|
+
* ids a refinement can return on its own — the probe and the Muse
|
|
2515
|
+
* contributor id — all have to be named, or `npx @openai/codex` would have
|
|
2516
|
+
* quietly stopped requiring a sandbox by gaining a better class.
|
|
2517
|
+
*/
|
|
2518
|
+
...HARNESS_RULES.map((rule) => rule.id),
|
|
2519
|
+
HARNESS_PROBE_RULE,
|
|
2520
|
+
MUSE_CONTRIBUTOR_RULE,
|
|
1930
2521
|
];
|
|
1931
2522
|
/**
|
|
1932
2523
|
* Every class the table can emit, for docs and for the dogfood test.
|
|
@@ -1953,8 +2544,45 @@ export const CLASSIFIER_CLASSES = (() => {
|
|
|
1953
2544
|
seen.add(CREDENTIAL_CLASS);
|
|
1954
2545
|
seen.add("files.write.workspace");
|
|
1955
2546
|
seen.add("read.shell");
|
|
2547
|
+
// APRV-347: emitted from `classifySegment`'s tail rather than from a row of
|
|
2548
|
+
// the table, because it is a refinement of `read.shell` against roots the
|
|
2549
|
+
// CALLER resolved and no binary implies it on its own.
|
|
2550
|
+
seen.add(READ_OUT_OF_SCOPE_CLASS);
|
|
1956
2551
|
return [...seen].sort();
|
|
1957
2552
|
})();
|
|
2553
|
+
/**
|
|
2554
|
+
* The class the DAEMON's own cadence advance is gated as (APRV-382).
|
|
2555
|
+
*
|
|
2556
|
+
* The sub-class exists because the policy grammar has no actor condition and
|
|
2557
|
+
* this repository wanted one: an advance publishes records the log already
|
|
2558
|
+
* holds, so the daemon may make it unattended, while the same act from a
|
|
2559
|
+
* session in a worktree or a human terminal stays supervised. Two classes are
|
|
2560
|
+
* how that is written down, and which of them a cycle asks under is decided by
|
|
2561
|
+
* `core/advance-cycle.ts` from the running process, never from an argument.
|
|
2562
|
+
*
|
|
2563
|
+
* NO COMMAND SPELLS IT, on purpose. `approval log advance` classifies
|
|
2564
|
+
* `log.advance` whoever types it, so the looser line is unreachable from a
|
|
2565
|
+
* shell an agent can drive: it is reached only from inside the daemon process,
|
|
2566
|
+
* which an agent cannot become without a `gate.self` command this policy holds
|
|
2567
|
+
* at the manual default.
|
|
2568
|
+
*/
|
|
2569
|
+
export const ADVANCE_DAEMON_CLASS = "log.advance.daemon";
|
|
2570
|
+
/**
|
|
2571
|
+
* Classes this RUNTIME emits for its own gated actions, which no command spells.
|
|
2572
|
+
*
|
|
2573
|
+
* Separate from {@link CLASSIFIER_CLASSES}, which is the binary table's own set
|
|
2574
|
+
* and is what `docs/claude-code-hook.md` documents row by row. A class here is
|
|
2575
|
+
* emitted by a runtime cycle that registers and requests it directly — the
|
|
2576
|
+
* daemon's advance is the first — so a policy declaring it is declaring a line
|
|
2577
|
+
* that CAN fire, and the reachability check `core/policy-expectations.ts` runs
|
|
2578
|
+
* at the amendment ceremony must say so. Without this list that ceremony would
|
|
2579
|
+
* refuse the line `unreachable`, which is a true statement about the command
|
|
2580
|
+
* classifier and a false one about the runtime.
|
|
2581
|
+
*
|
|
2582
|
+
* Adding a name here is a claim that some path in this codebase asks the gate
|
|
2583
|
+
* for that class, and widening it is a reviewable diff.
|
|
2584
|
+
*/
|
|
2585
|
+
export const RUNTIME_CLASSES = [ADVANCE_DAEMON_CLASS];
|
|
1958
2586
|
/**
|
|
1959
2587
|
* Can the classifier emit `actionClass` for a project whose policy carries
|
|
1960
2588
|
* these `protected_paths`? (APRV-266.)
|
|
@@ -1970,10 +2598,17 @@ export const CLASSIFIER_CLASSES = (() => {
|
|
|
1970
2598
|
* A routed name is reachable exactly when some entry routes to it. A
|
|
1971
2599
|
* `policy.edit.spec` rule in a policy whose `protected_paths` routes nothing to
|
|
1972
2600
|
* it is a line that will never fire, and saying so is the whole point.
|
|
2601
|
+
*
|
|
2602
|
+
* {@link RUNTIME_CLASSES} is the third answer (APRV-382): a class no command
|
|
2603
|
+
* spells and a runtime cycle asks for directly is reachable in every project,
|
|
2604
|
+
* with no policy entry needed, because the path that emits it is in this
|
|
2605
|
+
* codebase rather than in the operator's file.
|
|
1973
2606
|
*/
|
|
1974
2607
|
export function emittableClass(actionClass, protectedPaths = []) {
|
|
1975
2608
|
if (CLASSIFIER_CLASSES.includes(actionClass))
|
|
1976
2609
|
return true;
|
|
2610
|
+
if (RUNTIME_CLASSES.includes(actionClass))
|
|
2611
|
+
return true;
|
|
1977
2612
|
if (!POLICY_EDIT_SUBCLASS.test(actionClass))
|
|
1978
2613
|
return false;
|
|
1979
2614
|
return protectedPaths.some((entry) => parseProtectedEntry(entry)?.routed === actionClass);
|
|
@@ -2215,6 +2850,11 @@ function classifySegment(segment, protectedPaths, context) {
|
|
|
2215
2850
|
}
|
|
2216
2851
|
let cls = refined === null ? rule.class : refined.class;
|
|
2217
2852
|
let ruleId = refined === null ? rule.id : refined.rule;
|
|
2853
|
+
// The value a refinement bound, when one did (APRV-352). Same field and same
|
|
2854
|
+
// meaning as the protected-path binding above: the words the classifier
|
|
2855
|
+
// matched, verbatim, so an approver is told WHICH refs a deletion names
|
|
2856
|
+
// rather than being handed a class and left to re-read the command.
|
|
2857
|
+
const boundPath = refined !== null && "path" in refined ? refined.path : undefined;
|
|
2218
2858
|
// A protected path anywhere in an effectful segment takes that path's class:
|
|
2219
2859
|
// the command is editing the gate, whatever else it is doing. Every
|
|
2220
2860
|
// positional is scanned, source and destination alike, so `cp` stays
|
|
@@ -2233,7 +2873,31 @@ function classifySegment(segment, protectedPaths, context) {
|
|
|
2233
2873
|
cls = "files.write.workspace";
|
|
2234
2874
|
ruleId = "redirect-write";
|
|
2235
2875
|
}
|
|
2236
|
-
|
|
2876
|
+
// APRV-347, last because it is the narrowest: a read whose target the TEXT
|
|
2877
|
+
// places outside every root the caller named. Only `read.shell` is scoped —
|
|
2878
|
+
// `read.web` reaches no file, and a segment that has already taken a
|
|
2879
|
+
// protected, credential or write class is not a read at all. The relative
|
|
2880
|
+
// and symlinked cases are deliberately NOT decided here; they are left at
|
|
2881
|
+
// `read.shell` for the hook's disk pass, which tightens and never loosens.
|
|
2882
|
+
if (cls === "read.shell" && (context.readRoots ?? []).length > 0) {
|
|
2883
|
+
const escaped = escapedReadTarget(basename, positionals, args, context.readRoots ?? []);
|
|
2884
|
+
if (escaped !== null) {
|
|
2885
|
+
return {
|
|
2886
|
+
ok: true,
|
|
2887
|
+
class: READ_OUT_OF_SCOPE_CLASS,
|
|
2888
|
+
rule: escaped.rule,
|
|
2889
|
+
path: escaped.path,
|
|
2890
|
+
...(sandbox === null ? {} : { sandbox }),
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
return {
|
|
2895
|
+
ok: true,
|
|
2896
|
+
class: cls,
|
|
2897
|
+
rule: ruleId,
|
|
2898
|
+
...(boundPath === undefined ? {} : { path: boundPath }),
|
|
2899
|
+
...(sandbox === null ? {} : { sandbox }),
|
|
2900
|
+
};
|
|
2237
2901
|
}
|
|
2238
2902
|
/**
|
|
2239
2903
|
* Classify a shell command line into the classes it would produce.
|
|
@@ -2270,6 +2934,30 @@ export function classifyCommand(command, protectedPaths = [], context = {}) {
|
|
|
2270
2934
|
const segments = [];
|
|
2271
2935
|
const classes = [];
|
|
2272
2936
|
for (const segment of lexed.segments) {
|
|
2937
|
+
// APRV-380. A LOGIN-SHELL WRAPPER is classified by the script it runs.
|
|
2938
|
+
// `loginShellScript` states the exact shape and why this is not the second
|
|
2939
|
+
// parser `OPAQUE_BINS` refuses; the inner classification is run with
|
|
2940
|
+
// `unwrapShell: false`, so a shell nested inside the script stays opaque
|
|
2941
|
+
// and the recursion is one level deep by construction.
|
|
2942
|
+
const script = context.unwrapShell === false ? null : loginShellScript(segment);
|
|
2943
|
+
if (script !== null) {
|
|
2944
|
+
const inner = classifyCommand(script, protectedPaths, { ...context, unwrapShell: false });
|
|
2945
|
+
if (!inner.ok) {
|
|
2946
|
+
// The refusal is the INNER one, reported against the inner segment: an
|
|
2947
|
+
// operator told `hook-opaque` for `zsh` learns nothing, and one told
|
|
2948
|
+
// which part of their script could not be read can rewrite it.
|
|
2949
|
+
return inner;
|
|
2950
|
+
}
|
|
2951
|
+
// Spliced rather than collapsed into one segment: a script is a command
|
|
2952
|
+
// line and its parts have their own classes, which is the thing a
|
|
2953
|
+
// one-class answer would lose.
|
|
2954
|
+
for (const found of inner.segments) {
|
|
2955
|
+
segments.push(found);
|
|
2956
|
+
if (!classes.includes(found.class))
|
|
2957
|
+
classes.push(found.class);
|
|
2958
|
+
}
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2273
2961
|
const outcome = classifySegment(segment, protectedPaths, context);
|
|
2274
2962
|
if (!outcome.ok) {
|
|
2275
2963
|
return { ok: false, code: outcome.code, segment: segment.text, detail: outcome.detail };
|