approval-md 0.2.0 → 0.4.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 +85 -877
- package/SPEC.md +150 -19
- package/cli.js +66 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +53 -1
- package/dist/src/channels/contract.js +304 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.js +6 -1
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +394 -12
- package/dist/src/channels/telegram.js +621 -68
- 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 +564 -34
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +177 -13
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
- package/dist/src/cli/channel-telegram-webhook.js +709 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +763 -28
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +10 -1
- package/dist/src/cli/channel.js +14 -1
- 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 +41 -6
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +1322 -122
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +17 -1
- package/dist/src/cli/execute.js +366 -22
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +14 -2
- package/dist/src/cli/help.js +395 -82
- 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 +828 -4
- package/dist/src/cli/hook.js +2972 -142
- 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/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +56 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.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 +7 -1
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- 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 +134 -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.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +510 -47
- 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-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +234 -2
- package/dist/src/core/attest.js +342 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +73 -3
- package/dist/src/core/audit.js +65 -5
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -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/command-class.d.ts +205 -0
- package/dist/src/core/command-class.js +1433 -28
- 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/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.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 +392 -6
- package/dist/src/core/gate.js +852 -24
- 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 +128 -4
- package/dist/src/core/harness-version.js +181 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.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-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +89 -22
- package/dist/src/core/log.js +197 -19
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +17 -1
- package/dist/src/core/policy-explain.js +48 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +65 -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 +81 -0
- package/dist/src/core/policy-match.js +87 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +75 -0
- package/dist/src/core/policy-proposal.js +110 -4
- 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 +182 -0
- package/dist/src/core/read-scope.js +256 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.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 +501 -0
- package/dist/src/core/sender-identity.js +609 -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 +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- 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/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.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/audit.d.ts +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +307 -7
- 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/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +30 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +158 -0
- package/dist/src/serve/archive.js +354 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +766 -0
- package/docs/cli-reference.md +2563 -73
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +6 -2
- package/schema/event.schema.json +595 -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/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -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/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -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/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +15 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -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-hermes.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -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/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +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 +100 -4
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `approval run` payload: the argv, the cwd, and the bytes of the script
|
|
3
|
+
* the argv names (SPEC.md §6.2, §10.4; APRV-401).
|
|
4
|
+
*
|
|
5
|
+
* ## The hazard this closes
|
|
6
|
+
*
|
|
7
|
+
* §6.2 defined this payload as "the argv array and cwd", and `approval run`
|
|
8
|
+
* recomputes it from the argv and cwd it is about to spawn. For a command that
|
|
9
|
+
* IS its own bytes (`npm update @types/node`) that is the whole action. For a
|
|
10
|
+
* command that names a script it is not: the payload binds a NAME, and the
|
|
11
|
+
* bytes behind that name are whatever the path holds at execution time. The
|
|
12
|
+
* requester controls the file between the request and the grant, so a human
|
|
13
|
+
* approved a path and the runtime executed a file. Found on a live grant during
|
|
14
|
+
* APRV-398, exploited by nobody, filed as APRV-401.
|
|
15
|
+
*
|
|
16
|
+
* The payload hash is supposed to BE the binding (§10.4: "A grant therefore
|
|
17
|
+
* approves specific bytes"), so the repair is to put the bytes inside it. A
|
|
18
|
+
* script edited between the declaration and the execution hashes differently,
|
|
19
|
+
* and the difference refuses `payload-mismatch` before the child is spawned and
|
|
20
|
+
* before anything is appended, exactly as a changed argv already did.
|
|
21
|
+
*
|
|
22
|
+
* ## The shape, as the operator ruled it (APRV-401 notes, 2026-09-21)
|
|
23
|
+
*
|
|
24
|
+
* > the script digest is PART OF THE BINDING, never advisory. Shape: when argv
|
|
25
|
+
* > names an interpreter (bash, sh, zsh, node, python3 and kin) followed by a
|
|
26
|
+
* > path that resolves to a regular file at request time, the runtime reads and
|
|
27
|
+
* > hashes the file into the bound payload, the card shows the digest, byte
|
|
28
|
+
* > count and path, and approval run re-reads the file and refuses
|
|
29
|
+
* > payload-mismatch if the bytes changed. No interpretation of the script and
|
|
30
|
+
* > no second pass for files it sources; the limit is documented. Out of scope:
|
|
31
|
+
* > piped installers (already opaque), sourced files, interpreters the
|
|
32
|
+
* > classifier does not name.
|
|
33
|
+
*
|
|
34
|
+
* So this is an INTERPRETER-KEYED rule and not "every argv word that happens to
|
|
35
|
+
* name a file". One script per invocation: the first word after the interpreter
|
|
36
|
+
* that resolves to a readable regular file, which is the interpreter's operand
|
|
37
|
+
* whether or not a subcommand comes first (`deno run job.ts`).
|
|
38
|
+
* {@link SCRIPT_INTERPRETERS} is the list, and every member of it is a name
|
|
39
|
+
* `core/command-class.ts` already knows: the six shells of its unwrappable set,
|
|
40
|
+
* `node` from its own branch, and `python`, `python3`, `perl`, `ruby` and
|
|
41
|
+
* `deno` from its inline-source table. A launcher neither file names binds
|
|
42
|
+
* nothing, which is the documented limit rather than an oversight.
|
|
43
|
+
*
|
|
44
|
+
* The one thing here that the ruling does not spell out is the DEGENERATE case:
|
|
45
|
+
* `./install.sh` with no interpreter word at all, where the kernel reads the
|
|
46
|
+
* shebang and the script is `argv[0]` itself. It is bound, because it is the
|
|
47
|
+
* same fact with the interpreter implied rather than typed, and leaving it out
|
|
48
|
+
* would be a hole in the rule shaped exactly like the rule.
|
|
49
|
+
*
|
|
50
|
+
* ## What is NOT bound, stated plainly
|
|
51
|
+
*
|
|
52
|
+
* - **An inline program.** `bash -c '…'`, `node -e '…'`, `python3 -c '…'`: the
|
|
53
|
+
* program is a word of the argv, so it was always bound, and there is no file
|
|
54
|
+
* to read. The inline flags per interpreter are in {@link SCRIPT_INTERPRETERS}.
|
|
55
|
+
* - **Nothing resolved through PATH.** The interpreter itself is not hashed. Its
|
|
56
|
+
* bytes are the host's rather than the requester's, and resolving a program
|
|
57
|
+
* name through `PATH` would make the payload a function of the environment the
|
|
58
|
+
* process happens to hold, which §11 says is never read implicitly.
|
|
59
|
+
* - **Nothing transitive.** A bound script that sources another file, curls a
|
|
60
|
+
* second one, or execs a third binds only its own bytes. The digest is a
|
|
61
|
+
* binding, not a sandbox.
|
|
62
|
+
* - **A piped installer.** `curl … | sh` has no file operand and is opaque to
|
|
63
|
+
* the classifier for the same reason it is unbindable here.
|
|
64
|
+
* - **The instant between hashing and spawning.** The digest is taken, then
|
|
65
|
+
* `execution.started` is appended, then the child is spawned. A file rewritten
|
|
66
|
+
* inside that window runs unbound. Closing it needs the executor to hold the
|
|
67
|
+
* bytes rather than the path, which is the adapter contract of §10.4.
|
|
68
|
+
*
|
|
69
|
+
* ## Absence is not a claim
|
|
70
|
+
*
|
|
71
|
+
* `script` is omitted when nothing is bound, and that omission is deliberate: an
|
|
72
|
+
* invocation naming no readable script hashes to exactly the value it hashed
|
|
73
|
+
* before this module existed, so every record already in the log, and every
|
|
74
|
+
* declaration already written into a task file, verifies unchanged. What the
|
|
75
|
+
* absence does NOT say is "this runtime checked and there was nothing" — it is
|
|
76
|
+
* the ordinary shape of a payload with no script to bind, and a reader wanting
|
|
77
|
+
* to know whether a digest was possible reads the argv, which is right there.
|
|
78
|
+
*
|
|
79
|
+
* Every skip is safe for one reason, and it is the same reason in each case: the
|
|
80
|
+
* value is recomputed at execution by this same rule. A file that was
|
|
81
|
+
* unreadable at declaration time and readable at execution time gains an entry
|
|
82
|
+
* and refuses; one that existed and was deleted loses one and refuses. The rule
|
|
83
|
+
* does not have to decide correctly what a command will do, because both ends
|
|
84
|
+
* apply it to the same tree and any disagreement is a refusal.
|
|
85
|
+
*
|
|
86
|
+
* Reads the filesystem, by design and by necessity: at most one file per call,
|
|
87
|
+
* and only the one the argv names. Writes nothing, appends nothing, reads no
|
|
88
|
+
* log, no policy, no environment and no clock.
|
|
89
|
+
*/
|
|
90
|
+
import { createHash } from "node:crypto";
|
|
91
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
92
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
93
|
+
/** The payload key carrying the bound script. Omitted when there is none. */
|
|
94
|
+
export const BOUND_SCRIPT_KEY = "script";
|
|
95
|
+
/**
|
|
96
|
+
* How much of a file is read at once while digesting it.
|
|
97
|
+
*
|
|
98
|
+
* Chunked rather than `readFileSync`, because a script may be of any size and
|
|
99
|
+
* the executor must not be the thing that runs out of memory: a refusal to
|
|
100
|
+
* compute the binding is a refusal to execute.
|
|
101
|
+
*/
|
|
102
|
+
export const DIGEST_CHUNK_BYTES = 64 * 1024;
|
|
103
|
+
/**
|
|
104
|
+
* The interpreters whose script operand is bound, and the flags that mean
|
|
105
|
+
* "the program is inline, there is no file".
|
|
106
|
+
*
|
|
107
|
+
* Every name here is one `core/command-class.ts` already knows, which is the
|
|
108
|
+
* ruling's "interpreters the classifier does not name" read as a positive list:
|
|
109
|
+
* the six shells of its unwrappable set, `node` from its own classification
|
|
110
|
+
* branch, and `python`, `python3`, `perl`, `ruby`, `deno` from its
|
|
111
|
+
* inline-source table. The list is duplicated rather than imported because
|
|
112
|
+
* those tables are private to that module and a public export of them would be
|
|
113
|
+
* a second consumer of a decision that belongs to classification; the comment
|
|
114
|
+
* is the seam, and a name added there without being added here binds nothing,
|
|
115
|
+
* which fails toward the old behaviour rather than toward a wrong digest.
|
|
116
|
+
*
|
|
117
|
+
* The shell inline flag is `-c` with any run of `l` and `i` before it, which is
|
|
118
|
+
* `INLINE_SCRIPT_FLAG` in that module, spelled here as the same regular
|
|
119
|
+
* expression for the same reason.
|
|
120
|
+
*/
|
|
121
|
+
export const SCRIPT_INTERPRETERS = new Map([
|
|
122
|
+
["bash", [/^-[li]*c$/u]],
|
|
123
|
+
["sh", [/^-[li]*c$/u]],
|
|
124
|
+
["zsh", [/^-[li]*c$/u]],
|
|
125
|
+
["dash", [/^-[li]*c$/u]],
|
|
126
|
+
["ksh", [/^-[li]*c$/u]],
|
|
127
|
+
["fish", [/^-[li]*c$/u, "--command"]],
|
|
128
|
+
["node", ["-e", "--eval", "-p", "--print", "--input-type=module"]],
|
|
129
|
+
["nodejs", ["-e", "--eval", "-p", "--print"]],
|
|
130
|
+
["python", ["-c"]],
|
|
131
|
+
["python3", ["-c"]],
|
|
132
|
+
["perl", ["-e", "-E"]],
|
|
133
|
+
["ruby", ["-e"]],
|
|
134
|
+
["deno", ["eval"]],
|
|
135
|
+
]);
|
|
136
|
+
/**
|
|
137
|
+
* The digest and size of one file, read in bounded chunks, or `null` when it
|
|
138
|
+
* cannot be read at all.
|
|
139
|
+
*
|
|
140
|
+
* `null` for every failure — gone, a directory, a device, permission denied, a
|
|
141
|
+
* disappearing file, a read error partway — and the reason is not recorded
|
|
142
|
+
* anywhere. Recording it would put a local errno inside hashed material, so two
|
|
143
|
+
* machines that agree about the bytes could disagree about the payload; and the
|
|
144
|
+
* only thing a reader could do with it is treat "unreadable" as a state, which
|
|
145
|
+
* the recomputation already handles better than any field could.
|
|
146
|
+
*/
|
|
147
|
+
function fileDigest(path) {
|
|
148
|
+
let fd;
|
|
149
|
+
try {
|
|
150
|
+
const stats = statSync(path);
|
|
151
|
+
if (!stats.isFile())
|
|
152
|
+
return null;
|
|
153
|
+
fd = openSync(path, "r");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const hash = createHash("sha256");
|
|
159
|
+
const buffer = Buffer.allocUnsafe(DIGEST_CHUNK_BYTES);
|
|
160
|
+
let bytes = 0;
|
|
161
|
+
try {
|
|
162
|
+
let read = readSync(fd, buffer, 0, DIGEST_CHUNK_BYTES, null);
|
|
163
|
+
while (read > 0) {
|
|
164
|
+
hash.update(buffer.subarray(0, read));
|
|
165
|
+
bytes += read;
|
|
166
|
+
read = readSync(fd, buffer, 0, DIGEST_CHUNK_BYTES, null);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
try {
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// A close that fails has nothing to do with the bytes already hashed.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { bytes, sha256: hash.digest("hex") };
|
|
181
|
+
}
|
|
182
|
+
/** The last path segment of an argv word: `/usr/bin/bash` reads as `bash`. */
|
|
183
|
+
function basenameOf(word) {
|
|
184
|
+
const segments = word.split("/");
|
|
185
|
+
return segments[segments.length - 1] ?? word;
|
|
186
|
+
}
|
|
187
|
+
/** Does `word` mean "the program is inline" for this interpreter? */
|
|
188
|
+
function isInlineProgramFlag(word, markers) {
|
|
189
|
+
return markers.some((marker) => typeof marker === "string" ? word === marker : marker.test(word));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The words of an interpreter invocation that could be its script, in order,
|
|
193
|
+
* or `null` when the program is inline and there is no file at all.
|
|
194
|
+
*
|
|
195
|
+
* An inline-program marker anywhere in the options answers `null` rather than
|
|
196
|
+
* "keep looking": the program is a word of the argv and already bound, and a
|
|
197
|
+
* file that happens to share its spelling is a file the command never opens.
|
|
198
|
+
*
|
|
199
|
+
* Otherwise every non-option word is a candidate, in argv order, and the caller
|
|
200
|
+
* binds the first that resolves to a readable regular file. Not just the first
|
|
201
|
+
* non-option word, because interpreters take SUBCOMMANDS: `deno run job.ts`
|
|
202
|
+
* would otherwise offer `run` and stop, and `python3 -m pkg job.py` would offer
|
|
203
|
+
* `pkg`. Walking on cannot bind anything the argv does not name, and the worst
|
|
204
|
+
* case is the harmless direction — a command whose script is missing and whose
|
|
205
|
+
* later argument happens to be a file binds that file, which is one more thing
|
|
206
|
+
* bound rather than one fewer.
|
|
207
|
+
*
|
|
208
|
+
* An option that takes a separate path value is the one place this can pick the
|
|
209
|
+
* wrong file (`node --require ./a.js job.js` binds `./a.js`), and both are
|
|
210
|
+
* files that invocation executes.
|
|
211
|
+
*/
|
|
212
|
+
function scriptCandidateIndexes(argv, markers) {
|
|
213
|
+
const candidates = [];
|
|
214
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
215
|
+
const word = argv[index];
|
|
216
|
+
if (isInlineProgramFlag(word, markers))
|
|
217
|
+
return null;
|
|
218
|
+
if (word === "--")
|
|
219
|
+
continue;
|
|
220
|
+
if (word.startsWith("-") && word.length > 1)
|
|
221
|
+
continue;
|
|
222
|
+
candidates.push(index);
|
|
223
|
+
}
|
|
224
|
+
return candidates;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Where an argv word points, resolved against the payload's own `cwd`.
|
|
228
|
+
*
|
|
229
|
+
* The cwd is the payload's, never `process.cwd()`: the payload already carries
|
|
230
|
+
* the directory the child will run in, and resolving against anything else
|
|
231
|
+
* would make the binding a function of where the hashing process happened to
|
|
232
|
+
* stand.
|
|
233
|
+
*/
|
|
234
|
+
function resolveArgvPath(word, cwd) {
|
|
235
|
+
if (word === "" || word === "-" || word === "--")
|
|
236
|
+
return null;
|
|
237
|
+
if (word.includes("\0"))
|
|
238
|
+
return null;
|
|
239
|
+
return isAbsolute(word) ? word : resolvePath(cwd, word);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The script this argv runs, bound by its bytes, or `null` (APRV-401).
|
|
243
|
+
*
|
|
244
|
+
* Two shapes, and only two: an interpreter this module names followed by a path
|
|
245
|
+
* operand, and a path at `argv[0]` that the kernel will read a shebang from.
|
|
246
|
+
* Anything else binds nothing and the payload is what it always was.
|
|
247
|
+
*/
|
|
248
|
+
export function boundScript(argv, cwd) {
|
|
249
|
+
const program = argv[0];
|
|
250
|
+
if (program === undefined || program === "")
|
|
251
|
+
return null;
|
|
252
|
+
const markers = SCRIPT_INTERPRETERS.get(basenameOf(program));
|
|
253
|
+
if (markers !== undefined) {
|
|
254
|
+
const candidates = scriptCandidateIndexes(argv, markers);
|
|
255
|
+
if (candidates === null)
|
|
256
|
+
return null;
|
|
257
|
+
for (const index of candidates) {
|
|
258
|
+
const path = resolveArgvPath(argv[index], cwd);
|
|
259
|
+
if (path === null)
|
|
260
|
+
continue;
|
|
261
|
+
const digest = fileDigest(path);
|
|
262
|
+
if (digest !== null)
|
|
263
|
+
return { argv_index: index, path, ...digest };
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
// The degenerate case: no interpreter word, so the script IS argv[0] and the
|
|
268
|
+
// interpreter is whatever its shebang names. A program with no separator in
|
|
269
|
+
// it is a PATH lookup and is not this (see the module header).
|
|
270
|
+
if (!isAbsolute(program) && !program.includes("/"))
|
|
271
|
+
return null;
|
|
272
|
+
const path = resolveArgvPath(program, cwd);
|
|
273
|
+
if (path === null)
|
|
274
|
+
return null;
|
|
275
|
+
const digest = fileDigest(path);
|
|
276
|
+
return digest === null ? null : { argv_index: 0, path, ...digest };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The value `approval run`'s `payload_hash` is taken over (SPEC.md §6.2).
|
|
280
|
+
*
|
|
281
|
+
* `{argv, cwd}` when the argv names no readable script, byte for byte what it
|
|
282
|
+
* has always been; `{argv, cwd, script}` when it does. The caller canonicalizes
|
|
283
|
+
* and hashes it — this function decides WHAT is bound and never how it is
|
|
284
|
+
* digested into an approval.
|
|
285
|
+
*/
|
|
286
|
+
export function runPayloadValue(argv, cwd) {
|
|
287
|
+
const script = boundScript(argv, cwd);
|
|
288
|
+
const payload = { argv: [...argv], cwd };
|
|
289
|
+
if (script !== null)
|
|
290
|
+
payload.script = script;
|
|
291
|
+
return payload;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* One line naming the bound script, for a refusal message or a terminal.
|
|
295
|
+
*
|
|
296
|
+
* Not a rendering an approver signs: the canonical renderer owns that, and this
|
|
297
|
+
* is the executor explaining to an agent WHICH file moved.
|
|
298
|
+
*/
|
|
299
|
+
export function describeBoundScript(script) {
|
|
300
|
+
if (script === null)
|
|
301
|
+
return "no script";
|
|
302
|
+
return `argv[${String(script.argv_index)}] ${script.path}, ${String(script.bytes)} bytes, sha256 ${script.sha256}`;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=run-payload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-payload.js","sourceRoot":"","sources":["../../../src/core/run-payload.ts"],"names":[],"mappings":"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"}
|
|
@@ -153,9 +153,90 @@ export interface EgressAllowance {
|
|
|
153
153
|
* reach the profile. Directories deny their whole subtree.
|
|
154
154
|
*/
|
|
155
155
|
readonly denyRead: readonly string[];
|
|
156
|
+
/**
|
|
157
|
+
* WRITE CONFINEMENT (APRV-325.3): the only absolute subtrees the child may
|
|
158
|
+
* write, or `undefined` for the ordinary egress-only profile.
|
|
159
|
+
*
|
|
160
|
+
* This flips the posture for one rule family and one only. The header explains
|
|
161
|
+
* why the rest of this module is a deny-LIST: a `(deny default)` profile
|
|
162
|
+
* spends itself re-allowing dyld, the process's own binary and every temporary
|
|
163
|
+
* directory, and a control that breaks ordinary development is a control that
|
|
164
|
+
* gets switched off. That reasoning holds for reads and for everything else,
|
|
165
|
+
* and it does NOT hold for writes in a confined session, where the whole
|
|
166
|
+
* property being enforced is "this shell cannot change the canonical
|
|
167
|
+
* workspace". A write deny-list would have to enumerate every path worth
|
|
168
|
+
* protecting; this names the handful worth writing, so a path nobody thought
|
|
169
|
+
* of is denied rather than forgotten.
|
|
170
|
+
*
|
|
171
|
+
* `/dev` is allowed alongside them, unconditionally. Writes there are process
|
|
172
|
+
* I/O rather than filesystem state — `/dev/null`, `/dev/stdout`, `/dev/tty`,
|
|
173
|
+
* the pty a shell needs — and denying them kills the child before it can
|
|
174
|
+
* demonstrate anything, which is the failure mode point 1 of the header
|
|
175
|
+
* records for `network-outbound` and unix sockets.
|
|
176
|
+
*
|
|
177
|
+
* An EMPTY array is meaningful and is not the same as `undefined`: it denies
|
|
178
|
+
* every write outside `/dev`. `undefined` emits no write rules at all.
|
|
179
|
+
*/
|
|
180
|
+
readonly writeAllow?: readonly string[];
|
|
181
|
+
/**
|
|
182
|
+
* The READ JAIL (APRV-347): absolute directories the child may read, with
|
|
183
|
+
* everything else denied.
|
|
184
|
+
*
|
|
185
|
+
* Absent or empty leaves the profile exactly as it was — allow-default with a
|
|
186
|
+
* deny-list — and the bytes are identical to the pre-APRV-347 profile, which
|
|
187
|
+
* a fixture test pins. That default is deliberate: a deny-default read
|
|
188
|
+
* profile is a much larger change to what ordinary development can do, and a
|
|
189
|
+
* control that breaks `npm run build` is a control someone switches off.
|
|
190
|
+
*
|
|
191
|
+
* When it IS set, the profile flips to `(deny file-read*)` plus a `subpath`
|
|
192
|
+
* allow for each of these roots and for the fixed runtime set a process needs
|
|
193
|
+
* to exist at all (see {@link RUNTIME_READ_PATHS}). `denyRead` still applies
|
|
194
|
+
* and is emitted AFTER the allows, so a credential file inside an allowed
|
|
195
|
+
* root stays unreadable: last match wins in SBPL, and the credential denial
|
|
196
|
+
* must be the last word.
|
|
197
|
+
*/
|
|
198
|
+
readonly allowRead?: readonly string[];
|
|
156
199
|
}
|
|
157
200
|
/** The default: nothing allowed, nothing denied beyond the network. */
|
|
158
201
|
export declare const DENY_ALL_EGRESS: EgressAllowance;
|
|
202
|
+
/**
|
|
203
|
+
* What a process must be able to read to exist, regardless of the jail.
|
|
204
|
+
*
|
|
205
|
+
* Deliberately a fixed compiled-in list rather than anything a caller supplies:
|
|
206
|
+
* a jail whose runtime set could be widened by the party under oversight is not
|
|
207
|
+
* a jail (SPEC.md §11.1, self-reported fields never reduce scrutiny).
|
|
208
|
+
*
|
|
209
|
+
* - `/usr/lib`, `/usr/share`, `/System`, `/Library` — dyld, the shared cache,
|
|
210
|
+
* ICU data and the frameworks every Mach-O binary links. A profile without
|
|
211
|
+
* them kills the process before `main`, which reads as the command failing
|
|
212
|
+
* rather than as the sandbox working.
|
|
213
|
+
* - `/private/var/db/dyld` and `/var/db/dyld` — the dyld shared cache, which
|
|
214
|
+
* moved out of `/usr/lib` and is opened by name.
|
|
215
|
+
* - `/dev` — `/dev/null`, `/dev/urandom`, `/dev/dtracehelper`, the tty.
|
|
216
|
+
* - `/bin`, `/usr/bin`, `/sbin`, `/usr/sbin`, `/opt/homebrew`, `/usr/local` —
|
|
217
|
+
* the interpreters and tools a build shells out to. The node binary's own
|
|
218
|
+
* realpath is added separately by {@link runtimeReadRoots}, because a Node
|
|
219
|
+
* installed by a version manager lives under the user's home and none of
|
|
220
|
+
* these covers it.
|
|
221
|
+
* - `/etc`, `/private/etc` — `resolv.conf`, `passwd`, the locale tables.
|
|
222
|
+
*
|
|
223
|
+
* `node_modules` is NOT here, and that is a stated limit rather than an
|
|
224
|
+
* oversight: a repository's dependencies live inside the repository, so they
|
|
225
|
+
* are covered by the gate root being a read root. A project whose dependencies
|
|
226
|
+
* sit OUTSIDE the root (a pnpm store elsewhere, a linked package) must name
|
|
227
|
+
* that directory in `read_scope.roots`, which is exactly the widening that key
|
|
228
|
+
* exists for. docs/sandboxed-exec.md says so beside the survey.
|
|
229
|
+
*
|
|
230
|
+
* **The temp roots are not here either**, and that one is load-bearing. They
|
|
231
|
+
* reach the profile through the CALLER — `resolveReadRoots` puts the session
|
|
232
|
+
* scratchpad and the system temp root in the effective read scope, so
|
|
233
|
+
* `approval run` and `approval sandbox` open them and build tooling keeps
|
|
234
|
+
* working — and a caller that passes narrower roots gets a narrower jail. A
|
|
235
|
+
* temp root compiled in here would have been a widening no operator could turn
|
|
236
|
+
* off and no test could demonstrate a denial against, which is how it was
|
|
237
|
+
* found.
|
|
238
|
+
*/
|
|
239
|
+
export declare const RUNTIME_READ_PATHS: readonly string[];
|
|
159
240
|
/**
|
|
160
241
|
* The files beside a log that hold credential material, for the profile's
|
|
161
242
|
* `denyRead`.
|
package/dist/src/core/sandbox.js
CHANGED
|
@@ -125,6 +125,77 @@ export const SANDBOX_STATES = [
|
|
|
125
125
|
];
|
|
126
126
|
/** The default: nothing allowed, nothing denied beyond the network. */
|
|
127
127
|
export const DENY_ALL_EGRESS = { loopback: false, denyRead: [] };
|
|
128
|
+
/**
|
|
129
|
+
* What a process must be able to read to exist, regardless of the jail.
|
|
130
|
+
*
|
|
131
|
+
* Deliberately a fixed compiled-in list rather than anything a caller supplies:
|
|
132
|
+
* a jail whose runtime set could be widened by the party under oversight is not
|
|
133
|
+
* a jail (SPEC.md §11.1, self-reported fields never reduce scrutiny).
|
|
134
|
+
*
|
|
135
|
+
* - `/usr/lib`, `/usr/share`, `/System`, `/Library` — dyld, the shared cache,
|
|
136
|
+
* ICU data and the frameworks every Mach-O binary links. A profile without
|
|
137
|
+
* them kills the process before `main`, which reads as the command failing
|
|
138
|
+
* rather than as the sandbox working.
|
|
139
|
+
* - `/private/var/db/dyld` and `/var/db/dyld` — the dyld shared cache, which
|
|
140
|
+
* moved out of `/usr/lib` and is opened by name.
|
|
141
|
+
* - `/dev` — `/dev/null`, `/dev/urandom`, `/dev/dtracehelper`, the tty.
|
|
142
|
+
* - `/bin`, `/usr/bin`, `/sbin`, `/usr/sbin`, `/opt/homebrew`, `/usr/local` —
|
|
143
|
+
* the interpreters and tools a build shells out to. The node binary's own
|
|
144
|
+
* realpath is added separately by {@link runtimeReadRoots}, because a Node
|
|
145
|
+
* installed by a version manager lives under the user's home and none of
|
|
146
|
+
* these covers it.
|
|
147
|
+
* - `/etc`, `/private/etc` — `resolv.conf`, `passwd`, the locale tables.
|
|
148
|
+
*
|
|
149
|
+
* `node_modules` is NOT here, and that is a stated limit rather than an
|
|
150
|
+
* oversight: a repository's dependencies live inside the repository, so they
|
|
151
|
+
* are covered by the gate root being a read root. A project whose dependencies
|
|
152
|
+
* sit OUTSIDE the root (a pnpm store elsewhere, a linked package) must name
|
|
153
|
+
* that directory in `read_scope.roots`, which is exactly the widening that key
|
|
154
|
+
* exists for. docs/sandboxed-exec.md says so beside the survey.
|
|
155
|
+
*
|
|
156
|
+
* **The temp roots are not here either**, and that one is load-bearing. They
|
|
157
|
+
* reach the profile through the CALLER — `resolveReadRoots` puts the session
|
|
158
|
+
* scratchpad and the system temp root in the effective read scope, so
|
|
159
|
+
* `approval run` and `approval sandbox` open them and build tooling keeps
|
|
160
|
+
* working — and a caller that passes narrower roots gets a narrower jail. A
|
|
161
|
+
* temp root compiled in here would have been a widening no operator could turn
|
|
162
|
+
* off and no test could demonstrate a denial against, which is how it was
|
|
163
|
+
* found.
|
|
164
|
+
*/
|
|
165
|
+
export const RUNTIME_READ_PATHS = [
|
|
166
|
+
"/usr/lib",
|
|
167
|
+
"/usr/share",
|
|
168
|
+
"/System",
|
|
169
|
+
"/Library",
|
|
170
|
+
"/private/var/db/dyld",
|
|
171
|
+
"/var/db/dyld",
|
|
172
|
+
"/dev",
|
|
173
|
+
"/bin",
|
|
174
|
+
"/sbin",
|
|
175
|
+
"/usr/bin",
|
|
176
|
+
"/usr/sbin",
|
|
177
|
+
"/usr/local",
|
|
178
|
+
"/opt/homebrew",
|
|
179
|
+
"/etc",
|
|
180
|
+
"/private/etc",
|
|
181
|
+
];
|
|
182
|
+
/**
|
|
183
|
+
* The runtime set plus this process's own interpreter.
|
|
184
|
+
*
|
|
185
|
+
* `process.execPath` is resolved because a version manager's `node` is usually
|
|
186
|
+
* a symlink, and a `subpath` rule matches what the KERNEL resolves rather than
|
|
187
|
+
* what the profile was handed (the lesson APRV-193's design lane paid for).
|
|
188
|
+
*/
|
|
189
|
+
function runtimeReadRoots() {
|
|
190
|
+
const roots = [...RUNTIME_READ_PATHS];
|
|
191
|
+
try {
|
|
192
|
+
roots.push(dirname(realpathSync(process.execPath)));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// A process that cannot resolve its own binary is one the jail cannot help.
|
|
196
|
+
}
|
|
197
|
+
return roots;
|
|
198
|
+
}
|
|
128
199
|
// ---------------------------------------------------------------------------
|
|
129
200
|
// The credential material
|
|
130
201
|
// ---------------------------------------------------------------------------
|
|
@@ -293,6 +364,39 @@ export function seatbeltProfile(allowance) {
|
|
|
293
364
|
if (allowance.loopback) {
|
|
294
365
|
lines.push(";; Carve-out: a suite that starts its own localhost server. A real widening.", '(allow network-outbound (remote ip "localhost:*"))');
|
|
295
366
|
}
|
|
367
|
+
// APRV-347, the read jail. Emitted between the egress rules and the
|
|
368
|
+
// credential denials, and the ORDER is the policy: SBPL takes the last
|
|
369
|
+
// matching rule, so the roots open the filesystem and `denyRead` closes the
|
|
370
|
+
// credential material inside them afterwards. An empty (or absent)
|
|
371
|
+
// `allowRead` emits nothing at all, which is why a profile built without it
|
|
372
|
+
// is byte-identical to the profile this function produced before this task.
|
|
373
|
+
const allowRead = allowance.allowRead ?? [];
|
|
374
|
+
if (allowRead.length > 0) {
|
|
375
|
+
lines.push(";; APRV-347: the read jail. Deny-default, then the roots and the fixed", ";; runtime set a process needs to start at all.", "(deny file-read*)");
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
for (const path of [...allowRead, ...runtimeReadRoots()]) {
|
|
378
|
+
const resolved = resolveForProfile(path);
|
|
379
|
+
if (seen.has(resolved))
|
|
380
|
+
continue;
|
|
381
|
+
seen.add(resolved);
|
|
382
|
+
lines.push(`(allow file-read* (subpath ${sbplString(resolved)}))`);
|
|
383
|
+
}
|
|
384
|
+
// The ROOT DIRECTORY needs `file-read*`, not metadata, and this was
|
|
385
|
+
// measured rather than reasoned: with every other directory a process needs
|
|
386
|
+
// opened by subpath and `/` allowed for metadata only, `/usr/bin/true`
|
|
387
|
+
// dies with SIGABRT before `main`. `/` alone, as a literal: it opens the
|
|
388
|
+
// top-level directory listing and no file in it.
|
|
389
|
+
lines.push('(allow file-read* (literal "/"))');
|
|
390
|
+
// `file-read-metadata` on every other ancestor of every allowed root:
|
|
391
|
+
// resolving a path means stat-ing each directory on the way down, and a
|
|
392
|
+
// jail that denied that would refuse `/Users/x/dev/muse` for lack of
|
|
393
|
+
// `/Users`. Metadata leaks a directory's existence, never its contents.
|
|
394
|
+
for (const ancestor of ancestorsOf([...seen])) {
|
|
395
|
+
if (ancestor === "/")
|
|
396
|
+
continue;
|
|
397
|
+
lines.push(`(allow file-read-metadata (literal ${sbplString(ancestor)}))`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
296
400
|
for (const path of allowance.denyRead) {
|
|
297
401
|
const resolved = resolveForProfile(path);
|
|
298
402
|
// `literal` for a file, `subpath` for a directory. Both are emitted for a
|
|
@@ -312,8 +416,91 @@ export function seatbeltProfile(allowance) {
|
|
|
312
416
|
if (!file)
|
|
313
417
|
lines.push(`(deny file-read* (subpath ${sbplString(resolved)}))`);
|
|
314
418
|
}
|
|
419
|
+
if (allowance.writeAllow !== undefined) {
|
|
420
|
+
lines.push(";; APRV-325.3: write confinement. Deny-DEFAULT for this rule family only,", ";; so a path nobody thought of is denied rather than forgotten.", "(deny file-write*)", ";; /dev is process I/O, not filesystem state. Denying it kills a shell", ";; before it can do anything, for the reason a bare network-outbound deny does.", '(allow file-write* (subpath "/dev"))');
|
|
421
|
+
for (const path of allowance.writeAllow) {
|
|
422
|
+
lines.push(`(allow file-write* (subpath ${sbplString(resolveForProfile(path))}))`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
315
425
|
return `${lines.join("\n")}\n`;
|
|
316
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* The read jail, plus the directories the command being run lives in (APRV-347).
|
|
429
|
+
*
|
|
430
|
+
* You cannot run a program you may not read. `sandbox-exec` execs the RESOLVED
|
|
431
|
+
* path, so a `node` or `npm` installed outside the compiled-in runtime set — a
|
|
432
|
+
* version manager under `~`, a `.local/bin` shim — fails `execvp` with
|
|
433
|
+
* `Operation not permitted` and exits 71, which reads as the command failing
|
|
434
|
+
* rather than as the jail working.
|
|
435
|
+
*
|
|
436
|
+
* Two directories are opened: the resolved executable's own, and its PARENT.
|
|
437
|
+
* The parent is what makes a toolchain work rather than merely start: an
|
|
438
|
+
* interpreter's entry point lives at `<prefix>/bin/<name>` and the library it
|
|
439
|
+
* immediately requires lives at `<prefix>/lib` or beside it, so opening only
|
|
440
|
+
* `bin` gets a process that launches and cannot find itself. `npm` resolves to
|
|
441
|
+
* `…/lib/node_modules/npm/bin/npm-cli.js`, whose parent is the npm package, and
|
|
442
|
+
* that is the shape this rule is for.
|
|
443
|
+
*
|
|
444
|
+
* It is NOT a widening a caller chooses: it is derived from the argv this
|
|
445
|
+
* function was already given, and it does nothing at all when no jail was
|
|
446
|
+
* asked for. Anything a command needs BEYOND its own install prefix — a
|
|
447
|
+
* dependency store elsewhere, a linked package — is the operator's
|
|
448
|
+
* `read_scope.roots` to declare.
|
|
449
|
+
*/
|
|
450
|
+
function withExecutableRoots(allowance, command) {
|
|
451
|
+
const jail = allowance.allowRead ?? [];
|
|
452
|
+
if (jail.length === 0)
|
|
453
|
+
return allowance;
|
|
454
|
+
let real = command;
|
|
455
|
+
try {
|
|
456
|
+
real = realpathSync(command);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Not resolvable: the caller already handles a command that is not there.
|
|
460
|
+
}
|
|
461
|
+
const own = dirname(real);
|
|
462
|
+
// `dirname(command)` as well as `dirname(real)`: a PATH entry is usually a
|
|
463
|
+
// directory of symlinks (`~/.local/bin/npm` -> a version manager's tree), and
|
|
464
|
+
// traversing the link needs the link's own directory open, not just the
|
|
465
|
+
// target's. Leaving it out fails `execvp` before the program starts.
|
|
466
|
+
const roots = [own, dirname(command)];
|
|
467
|
+
// The prefix, behind a DEPTH FLOOR. `dirname("/bin")` is `/`, and a root of
|
|
468
|
+
// `/` would open the entire filesystem — a jail that opened everything and
|
|
469
|
+
// reported nothing, which is the worst failure this file can have. Two
|
|
470
|
+
// segments, the same floor the hook applies to a scratch root and for the
|
|
471
|
+
// same reason: a value derived from somewhere else must not be able to name
|
|
472
|
+
// the root of the disk.
|
|
473
|
+
const prefix = dirname(own);
|
|
474
|
+
if (prefix.split("/").filter((segment) => segment.length > 0).length >= 2) {
|
|
475
|
+
roots.push(prefix);
|
|
476
|
+
}
|
|
477
|
+
return { ...allowance, allowRead: [...jail, ...roots] };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Every proper ancestor directory of these paths, sorted and de-duplicated.
|
|
481
|
+
*
|
|
482
|
+
* `subpath` opens a subtree; it says nothing about the directories ABOVE it,
|
|
483
|
+
* and opening `/Users/carter/dev/muse` is useless if the process may not stat
|
|
484
|
+
* `/Users`, `/Users/carter` and `/Users/carter/dev` on the way down. Metadata
|
|
485
|
+
* only: the names of the intermediate directories leak, their contents do not.
|
|
486
|
+
*/
|
|
487
|
+
function ancestorsOf(paths) {
|
|
488
|
+
const out = new Set();
|
|
489
|
+
for (const path of paths) {
|
|
490
|
+
let at = dirname(path);
|
|
491
|
+
for (let depth = 0; depth < 64; depth += 1) {
|
|
492
|
+
if (at === "/" || at === "." || at === "")
|
|
493
|
+
break;
|
|
494
|
+
out.add(at);
|
|
495
|
+
const up = dirname(at);
|
|
496
|
+
if (up === at)
|
|
497
|
+
break;
|
|
498
|
+
at = up;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
out.add("/");
|
|
502
|
+
return [...out].sort();
|
|
503
|
+
}
|
|
317
504
|
/**
|
|
318
505
|
* A path as an SBPL string literal.
|
|
319
506
|
*
|
|
@@ -377,7 +564,9 @@ export function wrapForSandbox(mechanism, command, args, allowance = DENY_ALL_EG
|
|
|
377
564
|
}
|
|
378
565
|
const dir = mkdtempSync(join(realTmpdir(), "approval-sandbox-"));
|
|
379
566
|
const profile = join(dir, "egress-denied.sb");
|
|
380
|
-
writeFileSync(profile, seatbeltProfile(allowance), {
|
|
567
|
+
writeFileSync(profile, seatbeltProfile(withExecutableRoots(allowance, command)), {
|
|
568
|
+
mode: 0o600,
|
|
569
|
+
});
|
|
381
570
|
return {
|
|
382
571
|
command: SANDBOX_EXEC,
|
|
383
572
|
args: [...profileFlags(profile), command, ...args],
|