@nathapp/nax-agent 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
- package/dist/coding-tools/coding-tool-bash.js +44 -0
- package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
- package/dist/coding-tools/coding-tool-extras.js +56 -0
- package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
- package/dist/coding-tools/coding-tool-sandbox.js +215 -0
- package/dist/coding-tools/coding-tool-support.d.ts +132 -0
- package/dist/coding-tools/coding-tool-support.js +159 -0
- package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
- package/dist/coding-tools/universal-coding-tools.js +5 -0
- package/dist/command-interceptor/index.d.ts +55 -0
- package/dist/command-interceptor/index.js +60 -0
- package/dist/command-interceptor/shell.d.ts +36 -0
- package/dist/command-interceptor/shell.js +137 -0
- package/dist/command-safety/build.d.ts +19 -0
- package/dist/command-safety/build.js +47 -0
- package/dist/command-safety/guard.d.ts +32 -0
- package/dist/command-safety/guard.js +179 -0
- package/dist/command-safety/identifiers.d.ts +24 -0
- package/dist/command-safety/identifiers.js +20 -0
- package/dist/command-safety/index.d.ts +17 -0
- package/dist/command-safety/index.js +11 -0
- package/dist/command-safety/questions.d.ts +19 -0
- package/dist/command-safety/questions.js +60 -0
- package/dist/command-safety/row.d.ts +2 -0
- package/dist/command-safety/row.js +14 -0
- package/dist/command-safety/rule-scorer.d.ts +9 -0
- package/dist/command-safety/rule-scorer.js +391 -0
- package/dist/command-safety/shadow.d.ts +33 -0
- package/dist/command-safety/shadow.js +188 -0
- package/dist/command-safety/systemone-client.d.ts +16 -0
- package/dist/command-safety/systemone-client.js +92 -0
- package/dist/command-safety/tap.d.ts +37 -0
- package/dist/command-safety/tap.js +77 -0
- package/dist/command-safety/temp-only.d.ts +6 -0
- package/dist/command-safety/temp-only.js +215 -0
- package/dist/command-safety/tmp-write.d.ts +2 -0
- package/dist/command-safety/tmp-write.js +112 -0
- package/dist/command-safety/types.d.ts +185 -0
- package/dist/command-safety/types.js +21 -0
- package/dist/config/bash-approval.d.ts +33 -0
- package/dist/config/bash-approval.js +30 -0
- package/dist/config/catalog-overrides.d.ts +94 -0
- package/dist/config/catalog-overrides.js +7 -0
- package/dist/config/native-agent/index.d.ts +8 -0
- package/dist/config/native-agent/index.js +8 -0
- package/dist/config/schemas-sandbox.d.ts +31 -0
- package/dist/config/schemas-sandbox.js +45 -0
- package/dist/cost/core/index.d.ts +8 -0
- package/dist/cost/core/index.js +7 -0
- package/dist/cost/estimate.d.ts +34 -0
- package/dist/cost/estimate.js +65 -0
- package/dist/cost/model-spec.d.ts +28 -0
- package/dist/cost/model-spec.js +29 -0
- package/dist/cost/standard-types.d.ts +9 -0
- package/dist/cost/standard-types.js +1 -0
- package/dist/cost/usage-math.d.ts +18 -0
- package/dist/cost/usage-math.js +46 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +21 -0
- package/dist/infra/agent-logger.d.ts +22 -0
- package/dist/infra/agent-logger.js +19 -0
- package/dist/infra/credentials-config.d.ts +20 -0
- package/dist/infra/credentials-config.js +15 -0
- package/dist/infra/errors.d.ts +9 -0
- package/dist/infra/errors.js +11 -0
- package/dist/infra/index.d.ts +7 -0
- package/dist/infra/index.js +7 -0
- package/dist/infra/nax-error.d.ts +10 -0
- package/dist/infra/nax-error.js +16 -0
- package/dist/infra/spin-breaker/hash.d.ts +2 -0
- package/dist/infra/spin-breaker/hash.js +5 -0
- package/dist/infra/spin-breaker/index.d.ts +120 -0
- package/dist/infra/spin-breaker/index.js +340 -0
- package/dist/internal/agent-output-env.d.ts +44 -0
- package/dist/internal/agent-output-env.js +57 -0
- package/dist/internal/argv-exec.d.ts +58 -0
- package/dist/internal/argv-exec.js +224 -0
- package/dist/internal/bounded-io.d.ts +24 -0
- package/dist/internal/bounded-io.js +51 -0
- package/dist/internal/command-spec/index.d.ts +29 -0
- package/dist/internal/command-spec/index.js +36 -0
- package/dist/internal/describe-value-type.d.ts +8 -0
- package/dist/internal/describe-value-type.js +14 -0
- package/dist/internal/exec-framing.d.ts +18 -0
- package/dist/internal/exec-framing.js +22 -0
- package/dist/internal/file-lock.d.ts +91 -0
- package/dist/internal/file-lock.js +212 -0
- package/dist/internal/file-size.d.ts +2 -0
- package/dist/internal/file-size.js +12 -0
- package/dist/internal/git-add.d.ts +43 -0
- package/dist/internal/git-add.js +59 -0
- package/dist/internal/git-env.d.ts +59 -0
- package/dist/internal/git-env.js +93 -0
- package/dist/internal/git-exec.d.ts +58 -0
- package/dist/internal/git-exec.js +115 -0
- package/dist/internal/path-file-lock.d.ts +23 -0
- package/dist/internal/path-file-lock.js +25 -0
- package/dist/internal/process-alive.d.ts +42 -0
- package/dist/internal/process-alive.js +56 -0
- package/dist/internal/process-kill.d.ts +27 -0
- package/dist/internal/process-kill.js +54 -0
- package/dist/internal/realpath.d.ts +29 -0
- package/dist/internal/realpath.js +52 -0
- package/dist/internal/redact.d.ts +40 -0
- package/dist/internal/redact.js +161 -0
- package/dist/internal/shell-quote.d.ts +2 -0
- package/dist/internal/shell-quote.js +4 -0
- package/dist/internal/sort.d.ts +20 -0
- package/dist/internal/sort.js +20 -0
- package/dist/internal/strip-control-chars.d.ts +19 -0
- package/dist/internal/strip-control-chars.js +38 -0
- package/dist/internal/thenable.d.ts +10 -0
- package/dist/internal/thenable.js +15 -0
- package/dist/internal.d.ts +91 -0
- package/dist/internal.js +91 -0
- package/dist/native/adapter-deps.d.ts +46 -0
- package/dist/native/adapter-deps.js +43 -0
- package/dist/native/auth-types.d.ts +70 -0
- package/dist/native/auth-types.js +13 -0
- package/dist/native/auth.d.ts +133 -0
- package/dist/native/auth.js +362 -0
- package/dist/native/client.d.ts +50 -0
- package/dist/native/client.js +174 -0
- package/dist/native/complete.d.ts +33 -0
- package/dist/native/complete.js +88 -0
- package/dist/native/credentials/chained-store.d.ts +30 -0
- package/dist/native/credentials/chained-store.js +68 -0
- package/dist/native/credentials/change-guard.d.ts +51 -0
- package/dist/native/credentials/change-guard.js +136 -0
- package/dist/native/credentials/exec-source.d.ts +50 -0
- package/dist/native/credentials/exec-source.js +222 -0
- package/dist/native/credentials/fingerprint.d.ts +26 -0
- package/dist/native/credentials/fingerprint.js +174 -0
- package/dist/native/credentials/helper-process.d.ts +79 -0
- package/dist/native/credentials/helper-process.js +259 -0
- package/dist/native/credentials/helper-protocol.d.ts +36 -0
- package/dist/native/credentials/helper-protocol.js +79 -0
- package/dist/native/credentials/index.d.ts +60 -0
- package/dist/native/credentials/index.js +184 -0
- package/dist/native/errors.d.ts +34 -0
- package/dist/native/errors.js +109 -0
- package/dist/native/index.d.ts +25 -0
- package/dist/native/index.js +23 -0
- package/dist/native/model-resolver.d.ts +57 -0
- package/dist/native/model-resolver.js +73 -0
- package/dist/native/models.d.ts +116 -0
- package/dist/native/models.js +183 -0
- package/dist/native/session/ask-human.d.ts +15 -0
- package/dist/native/session/ask-human.js +22 -0
- package/dist/native/session/compaction.d.ts +94 -0
- package/dist/native/session/compaction.js +170 -0
- package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
- package/dist/native/session/handle-invalid-tool-call.js +159 -0
- package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
- package/dist/native/session/loop-events/cache-boundary.js +29 -0
- package/dist/native/session/loop-events/external-handler.d.ts +38 -0
- package/dist/native/session/loop-events/external-handler.js +193 -0
- package/dist/native/session/loop-events/index.d.ts +16 -0
- package/dist/native/session/loop-events/index.js +15 -0
- package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
- package/dist/native/session/loop-events/payload-guard.js +31 -0
- package/dist/native/session/loop-events/registry.d.ts +84 -0
- package/dist/native/session/loop-events/registry.js +305 -0
- package/dist/native/session/loop-events/types.d.ts +251 -0
- package/dist/native/session/loop-events/types.js +14 -0
- package/dist/native/session/loop-handlers.d.ts +73 -0
- package/dist/native/session/loop-handlers.js +137 -0
- package/dist/native/session/nudge.d.ts +29 -0
- package/dist/native/session/nudge.js +35 -0
- package/dist/native/session/rate-provenance.d.ts +17 -0
- package/dist/native/session/rate-provenance.js +39 -0
- package/dist/native/session/session.d.ts +139 -0
- package/dist/native/session/session.js +208 -0
- package/dist/native/session/tool-input-exemplar.d.ts +2 -0
- package/dist/native/session/tool-input-exemplar.js +66 -0
- package/dist/native/session/tool-input-validation.d.ts +15 -0
- package/dist/native/session/tool-input-validation.js +146 -0
- package/dist/native/session/tool-mapping.d.ts +18 -0
- package/dist/native/session/tool-mapping.js +27 -0
- package/dist/native/session/tool-result.d.ts +37 -0
- package/dist/native/session/tool-result.js +25 -0
- package/dist/native/session/transcript-store.d.ts +77 -0
- package/dist/native/session/transcript-store.js +200 -0
- package/dist/native/session/truncation-handler.d.ts +47 -0
- package/dist/native/session/truncation-handler.js +83 -0
- package/dist/native/session/turn-accumulator.d.ts +45 -0
- package/dist/native/session/turn-accumulator.js +67 -0
- package/dist/native/session/turn-ask-human.d.ts +23 -0
- package/dist/native/session/turn-ask-human.js +41 -0
- package/dist/native/session/turn-compaction-step.d.ts +74 -0
- package/dist/native/session/turn-compaction-step.js +128 -0
- package/dist/native/session/turn-complete-step.d.ts +77 -0
- package/dist/native/session/turn-complete-step.js +159 -0
- package/dist/native/session/turn-end-event.d.ts +3 -0
- package/dist/native/session/turn-end-event.js +17 -0
- package/dist/native/session/turn-events.d.ts +63 -0
- package/dist/native/session/turn-events.js +48 -0
- package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
- package/dist/native/session/turn-loop-round-trip.js +318 -0
- package/dist/native/session/turn-loop.d.ts +23 -0
- package/dist/native/session/turn-loop.js +250 -0
- package/dist/native/session/turn-result.d.ts +39 -0
- package/dist/native/session/turn-result.js +64 -0
- package/dist/native/session/turn-retry.d.ts +120 -0
- package/dist/native/session/turn-retry.js +182 -0
- package/dist/native/session/turn-tool-batch.d.ts +61 -0
- package/dist/native/session/turn-tool-batch.js +259 -0
- package/dist/native/session/turn-types.d.ts +164 -0
- package/dist/native/session/turn-types.js +36 -0
- package/dist/native/session-adapter.d.ts +56 -0
- package/dist/native/session-adapter.js +369 -0
- package/dist/native/session-affinity.d.ts +36 -0
- package/dist/native/session-affinity.js +43 -0
- package/dist/native/tier-providers.d.ts +18 -0
- package/dist/native/tier-providers.js +20 -0
- package/dist/permissions/approval-audit.d.ts +10 -0
- package/dist/permissions/approval-audit.js +22 -0
- package/dist/permissions/approvals-link.d.ts +20 -0
- package/dist/permissions/approvals-link.js +62 -0
- package/dist/permissions/approvals-store.d.ts +168 -0
- package/dist/permissions/approvals-store.js +259 -0
- package/dist/permissions/approvals-taint.d.ts +33 -0
- package/dist/permissions/approvals-taint.js +138 -0
- package/dist/permissions/ask-chain.d.ts +69 -0
- package/dist/permissions/ask-chain.js +32 -0
- package/dist/permissions/ask.d.ts +23 -0
- package/dist/permissions/ask.js +25 -0
- package/dist/permissions/bash-lex.d.ts +59 -0
- package/dist/permissions/bash-lex.js +279 -0
- package/dist/permissions/grammar.d.ts +12 -0
- package/dist/permissions/grammar.js +24 -0
- package/dist/permissions/index.d.ts +11 -0
- package/dist/permissions/index.js +9 -0
- package/dist/permissions/secret-spans.d.ts +19 -0
- package/dist/permissions/secret-spans.js +88 -0
- package/dist/permissions/types.d.ts +85 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/node-glob.d.ts +3 -0
- package/dist/runtime/node-glob.js +83 -0
- package/dist/runtime/node-runtime.d.ts +3 -0
- package/dist/runtime/node-runtime.js +126 -0
- package/dist/runtime/slot.d.ts +11 -0
- package/dist/runtime/slot.js +17 -0
- package/dist/runtime/types.d.ts +51 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/which.d.ts +8 -0
- package/dist/runtime/which.js +32 -0
- package/dist/sandbox/argv-quote.d.ts +11 -0
- package/dist/sandbox/argv-quote.js +13 -0
- package/dist/sandbox/defaults.d.ts +27 -0
- package/dist/sandbox/defaults.js +47 -0
- package/dist/sandbox/git-guards.d.ts +64 -0
- package/dist/sandbox/git-guards.js +161 -0
- package/dist/sandbox/index.d.ts +11 -0
- package/dist/sandbox/index.js +10 -0
- package/dist/sandbox/launcher.d.ts +37 -0
- package/dist/sandbox/launcher.js +160 -0
- package/dist/sandbox/messages.d.ts +17 -0
- package/dist/sandbox/messages.js +46 -0
- package/dist/sandbox/policy-builder.d.ts +44 -0
- package/dist/sandbox/policy-builder.js +149 -0
- package/dist/sandbox/policy-inputs.d.ts +53 -0
- package/dist/sandbox/policy-inputs.js +76 -0
- package/dist/sandbox/probe.d.ts +8 -0
- package/dist/sandbox/probe.js +66 -0
- package/dist/sandbox/registry.d.ts +17 -0
- package/dist/sandbox/registry.js +57 -0
- package/dist/sandbox/session-tmp.d.ts +20 -0
- package/dist/sandbox/session-tmp.js +95 -0
- package/dist/sandbox/srt-backend.d.ts +21 -0
- package/dist/sandbox/srt-backend.js +99 -0
- package/dist/sandbox/types.d.ts +102 -0
- package/dist/sandbox/types.js +1 -0
- package/dist/session/adapter-failure.d.ts +60 -0
- package/dist/session/adapter-failure.js +11 -0
- package/dist/session/agent-stream-events.d.ts +103 -0
- package/dist/session/agent-stream-events.js +6 -0
- package/dist/session/interaction-handler.d.ts +53 -0
- package/dist/session/interaction-handler.js +2 -0
- package/dist/session/no-op-interaction-handler.d.ts +9 -0
- package/dist/session/no-op-interaction-handler.js +11 -0
- package/dist/session/protocol-types.d.ts +12 -0
- package/dist/session/protocol-types.js +1 -0
- package/dist/session/session-types.d.ts +456 -0
- package/dist/session/session-types.js +64 -0
- package/dist/session/tool-descriptor.d.ts +23 -0
- package/dist/session/tool-descriptor.js +5 -0
- package/dist/session/turn-deadline.d.ts +17 -0
- package/dist/session/turn-deadline.js +23 -0
- package/dist/tools/ask-request.d.ts +35 -0
- package/dist/tools/ask-request.js +57 -0
- package/dist/tools/bash-cwd.d.ts +82 -0
- package/dist/tools/bash-cwd.js +55 -0
- package/dist/tools/bash.d.ts +69 -0
- package/dist/tools/bash.js +283 -0
- package/dist/tools/delete.d.ts +57 -0
- package/dist/tools/delete.js +154 -0
- package/dist/tools/denial-redirect.d.ts +42 -0
- package/dist/tools/denial-redirect.js +266 -0
- package/dist/tools/deny-paths.d.ts +35 -0
- package/dist/tools/deny-paths.js +76 -0
- package/dist/tools/edit-region.d.ts +50 -0
- package/dist/tools/edit-region.js +90 -0
- package/dist/tools/edit.d.ts +25 -0
- package/dist/tools/edit.js +114 -0
- package/dist/tools/exec-allowlist-text.d.ts +18 -0
- package/dist/tools/exec-allowlist-text.js +24 -0
- package/dist/tools/exec-guard.d.ts +69 -0
- package/dist/tools/exec-guard.js +151 -0
- package/dist/tools/git-commit.d.ts +59 -0
- package/dist/tools/git-commit.js +220 -0
- package/dist/tools/git-flags/index.d.ts +10 -0
- package/dist/tools/git-flags/index.js +10 -0
- package/dist/tools/git.d.ts +88 -0
- package/dist/tools/git.js +397 -0
- package/dist/tools/glob.d.ts +29 -0
- package/dist/tools/glob.js +153 -0
- package/dist/tools/grep.d.ts +21 -0
- package/dist/tools/grep.js +162 -0
- package/dist/tools/index.d.ts +36 -0
- package/dist/tools/index.js +28 -0
- package/dist/tools/narrow-grants.d.ts +19 -0
- package/dist/tools/narrow-grants.js +54 -0
- package/dist/tools/nax-owned-writes.d.ts +135 -0
- package/dist/tools/nax-owned-writes.js +259 -0
- package/dist/tools/package-managers-table.d.ts +146 -0
- package/dist/tools/package-managers-table.js +325 -0
- package/dist/tools/package-managers-types.d.ts +36 -0
- package/dist/tools/package-managers-types.js +6 -0
- package/dist/tools/package-managers.d.ts +85 -0
- package/dist/tools/package-managers.js +391 -0
- package/dist/tools/path-list.d.ts +38 -0
- package/dist/tools/path-list.js +62 -0
- package/dist/tools/policy-bash-raw.d.ts +19 -0
- package/dist/tools/policy-bash-raw.js +210 -0
- package/dist/tools/policy-bash.d.ts +64 -0
- package/dist/tools/policy-bash.js +232 -0
- package/dist/tools/policy-command-branch.d.ts +32 -0
- package/dist/tools/policy-command-branch.js +63 -0
- package/dist/tools/policy-input.d.ts +2 -0
- package/dist/tools/policy-input.js +10 -0
- package/dist/tools/policy-match.d.ts +75 -0
- package/dist/tools/policy-match.js +117 -0
- package/dist/tools/policy-paths-branch.d.ts +97 -0
- package/dist/tools/policy-paths-branch.js +314 -0
- package/dist/tools/policy.d.ts +65 -0
- package/dist/tools/policy.js +232 -0
- package/dist/tools/protected-paths.d.ts +26 -0
- package/dist/tools/protected-paths.js +8 -0
- package/dist/tools/provider-adapt.d.ts +4 -0
- package/dist/tools/provider-adapt.js +32 -0
- package/dist/tools/provider-advertise.d.ts +39 -0
- package/dist/tools/provider-advertise.js +50 -0
- package/dist/tools/provider-grants.d.ts +36 -0
- package/dist/tools/provider-grants.js +72 -0
- package/dist/tools/provider-sanitize.d.ts +19 -0
- package/dist/tools/provider-sanitize.js +53 -0
- package/dist/tools/provider-types.d.ts +41 -0
- package/dist/tools/provider-types.js +26 -0
- package/dist/tools/read-continuation.d.ts +147 -0
- package/dist/tools/read-continuation.js +176 -0
- package/dist/tools/read-file.d.ts +32 -0
- package/dist/tools/read-file.js +85 -0
- package/dist/tools/read.d.ts +25 -0
- package/dist/tools/read.js +182 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.js +67 -0
- package/dist/tools/request-capability.d.ts +13 -0
- package/dist/tools/request-capability.js +23 -0
- package/dist/tools/run-command-exec.d.ts +14 -0
- package/dist/tools/run-command-exec.js +161 -0
- package/dist/tools/run-command.d.ts +107 -0
- package/dist/tools/run-command.js +398 -0
- package/dist/tools/runtime-calltool.d.ts +146 -0
- package/dist/tools/runtime-calltool.js +223 -0
- package/dist/tools/runtime.d.ts +149 -0
- package/dist/tools/runtime.js +345 -0
- package/dist/tools/scratchpad.d.ts +20 -0
- package/dist/tools/scratchpad.js +197 -0
- package/dist/tools/spill.d.ts +81 -0
- package/dist/tools/spill.js +251 -0
- package/dist/tools/tool-audit.d.ts +171 -0
- package/dist/tools/tool-audit.js +183 -0
- package/dist/tools/truncate.d.ts +98 -0
- package/dist/tools/truncate.js +297 -0
- package/dist/tools/types.d.ts +145 -0
- package/dist/tools/types.js +24 -0
- package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
- package/dist/tools/verb-denial-argv-hint.js +40 -0
- package/dist/tools/write.d.ts +9 -0
- package/dist/tools/write.js +45 -0
- package/package.json +54 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The I/O half of the sandbox's git guards (#2198): the git-read files that
|
|
3
|
+
* redirect where git finds its config and hooks, as they exist on disk now.
|
|
4
|
+
*
|
|
5
|
+
* A sandboxed command that repoints one of these at an agent-authored
|
|
6
|
+
* directory (its own `config` with core.hooksPath / core.fsmonitor) makes
|
|
7
|
+
* nax's next UNSANDBOXED git run agent code. The redirecting files are:
|
|
8
|
+
* `<common>/commondir`, and for every linked worktree `worktrees/<id>/`
|
|
9
|
+
* `{gitdir, commondir, config.worktree}` plus the worktree's `.git` pointer.
|
|
10
|
+
*
|
|
11
|
+
* Only files that EXIST are denied here: srt on Linux stubs an absent deny
|
|
12
|
+
* with an empty file (host-side too, while the command runs), and git dies
|
|
13
|
+
* reading an empty `commondir` / `gitdir` -- inside the sandbox and in any
|
|
14
|
+
* concurrent nax git. An absent `<common>/commondir` is therefore guarded by
|
|
15
|
+
* a tripwire instead (strayCommonDirTripwire). `config.worktree` is the
|
|
16
|
+
* exception: an empty one is valid config, so it is always denied.
|
|
17
|
+
*
|
|
18
|
+
* The tripwire is what protects a main checkout, whose git dir sits inside the
|
|
19
|
+
* root write root. In a linked worktree the common dir is not a write root at
|
|
20
|
+
* all (WORKTREE_COMMON_WRITE_DIRS, #2211), so the file cannot be created there
|
|
21
|
+
* and the tripwire is only a second line.
|
|
22
|
+
*/
|
|
23
|
+
import { lstat, readdir, readFile, rm } from "node:fs/promises";
|
|
24
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
25
|
+
import { errorMessage } from "#src/infra/errors";
|
|
26
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
27
|
+
import { realOrRaw } from "#src/internal/realpath";
|
|
28
|
+
import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
|
|
29
|
+
const WORKTREES_DIR = "worktrees";
|
|
30
|
+
const COMMONDIR_FILE = "commondir";
|
|
31
|
+
const GITDIR_FILE = "gitdir";
|
|
32
|
+
export const WORKTREE_CONFIG_FILE = "config.worktree";
|
|
33
|
+
/**
|
|
34
|
+
* What a linked worktree's git writes in the SHARED common dir (#2211): loose
|
|
35
|
+
* and packed objects, refs and their reflogs (`reftable/` holds both when
|
|
36
|
+
* `extensions.refStorage=reftable`, git >= 2.45), and git-lfs's object store.
|
|
37
|
+
* Only these are write roots, never the common dir itself, so the redirecting
|
|
38
|
+
* files at its top level (`commondir`, `config`, `hooks/`) and every sibling
|
|
39
|
+
* worktree's admin dir lie outside the sandbox's writes. `modules/` is left
|
|
40
|
+
* out on purpose: it holds submodule git dirs, each with its own config.
|
|
41
|
+
*
|
|
42
|
+
* Cost: a lock file at the common dir's top level cannot be created, so a
|
|
43
|
+
* sandboxed command cannot rewrite `packed-refs` (deleting a packed branch or
|
|
44
|
+
* tag) or take the `gc --auto` lock; commits, loose ref updates and fetches
|
|
45
|
+
* into loose objects are unaffected. (A reftable repo locks inside
|
|
46
|
+
* `reftable/`, so it keeps ref deletion too.)
|
|
47
|
+
*/
|
|
48
|
+
export const WORKTREE_COMMON_WRITE_DIRS = ["objects", "refs", "logs", "reftable", "lfs"];
|
|
49
|
+
const DOT_GIT = ".git";
|
|
50
|
+
export const _gitGuardDeps = {
|
|
51
|
+
readdir,
|
|
52
|
+
exists: async (p) => {
|
|
53
|
+
try {
|
|
54
|
+
await lstat(p);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
readText: (p) => readFile(p, "utf8"),
|
|
62
|
+
remove: (p) => rm(p, { recursive: true, force: true }),
|
|
63
|
+
};
|
|
64
|
+
/** The directory holding the shared config/hooks: the git dir itself for a main checkout. */
|
|
65
|
+
export function commonDirOf(git) {
|
|
66
|
+
if (git.kind === "none")
|
|
67
|
+
return undefined;
|
|
68
|
+
return git.kind === "worktree" ? git.commonDir : git.gitDir;
|
|
69
|
+
}
|
|
70
|
+
/** A glob-named path would make every policy build throw (F1), so it is skipped, not emitted. */
|
|
71
|
+
function isLiteral(p) {
|
|
72
|
+
return !SANDBOX_GLOB_CHARS.test(realOrRaw(p));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The `<wt>/.git` pointer a `gitdir` file names -- only a literal `.git` path.
|
|
76
|
+
*
|
|
77
|
+
* git >= 2.48 with `worktree.useRelativePaths` (or `worktree add
|
|
78
|
+
* --relative-paths`) writes it relative to the admin dir `<common>/worktrees/<id>`
|
|
79
|
+
* -- computed from that dir's realpath, and read back by joining onto it -- so a
|
|
80
|
+
* relative target resolves against the admin dir's realpath. Resolving against
|
|
81
|
+
* the unresolved spelling would walk `..` lexically and miss a symlinked
|
|
82
|
+
* common dir.
|
|
83
|
+
*/
|
|
84
|
+
async function pointerOf(gitdirFile) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = (await _gitGuardDeps.readText(gitdirFile)).trim();
|
|
87
|
+
if (raw === "")
|
|
88
|
+
return [];
|
|
89
|
+
const target = isAbsolute(raw) ? raw : resolve(realOrRaw(dirname(gitdirFile)), raw);
|
|
90
|
+
return basename(target) === DOT_GIT && isLiteral(target) ? [target] : [];
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function existing(paths) {
|
|
97
|
+
const present = await Promise.all(paths.map((p) => _gitGuardDeps.exists(p)));
|
|
98
|
+
return paths.filter((_, i) => present[i]);
|
|
99
|
+
}
|
|
100
|
+
async function worktreeEntryFiles(adminDir) {
|
|
101
|
+
const gitdirFile = join(adminDir, GITDIR_FILE);
|
|
102
|
+
const pointers = await existing([gitdirFile, join(adminDir, COMMONDIR_FILE), ...(await pointerOf(gitdirFile))]);
|
|
103
|
+
return [...pointers, join(adminDir, WORKTREE_CONFIG_FILE)];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Every redirecting git file for the repo that `git` describes, for every
|
|
107
|
+
* worktree registered when the policy is built (literal paths: srt on Linux
|
|
108
|
+
* drops glob denies).
|
|
109
|
+
*/
|
|
110
|
+
export async function listGitGuardFiles(git) {
|
|
111
|
+
const common = commonDirOf(git);
|
|
112
|
+
if (common === undefined)
|
|
113
|
+
return [];
|
|
114
|
+
const worktrees = join(common, WORKTREES_DIR);
|
|
115
|
+
let names = [];
|
|
116
|
+
try {
|
|
117
|
+
const entries = await _gitGuardDeps.readdir(worktrees, { withFileTypes: true });
|
|
118
|
+
names = entries.filter((e) => e.isDirectory() && isLiteral(join(worktrees, e.name))).map((e) => e.name);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// no linked worktrees
|
|
122
|
+
}
|
|
123
|
+
const perWorktree = await Promise.all(names.map((name) => worktreeEntryFiles(join(worktrees, name))));
|
|
124
|
+
// Filtered again as a whole: an agent-made symlink can resolve a literal name to a glob path.
|
|
125
|
+
return [...(await existing([join(common, COMMONDIR_FILE)])), ...perWorktree.flat()].filter(isLiteral);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `<common>/commondir` never legitimately exists (git reads it only from a
|
|
129
|
+
* linked worktree's own git dir), and an absent one cannot be denied on Linux
|
|
130
|
+
* (see the header). So when it is absent at session start, one created by a
|
|
131
|
+
* sandboxed command is removed after that command, before nax's next git.
|
|
132
|
+
* Returns undefined when there is nothing to guard (no repo, or the file
|
|
133
|
+
* already exists -- listGitGuardFiles then denies it outright).
|
|
134
|
+
*/
|
|
135
|
+
export async function strayCommonDirTripwire(git, storyId) {
|
|
136
|
+
const common = commonDirOf(git);
|
|
137
|
+
if (common === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
const target = join(common, COMMONDIR_FILE);
|
|
140
|
+
if (await _gitGuardDeps.exists(target))
|
|
141
|
+
return undefined;
|
|
142
|
+
return async () => {
|
|
143
|
+
if (!(await _gitGuardDeps.exists(target)))
|
|
144
|
+
return;
|
|
145
|
+
const logger = getSafeLogger();
|
|
146
|
+
try {
|
|
147
|
+
await _gitGuardDeps.remove(target);
|
|
148
|
+
logger?.error("sandbox", "A sandboxed command created a git commondir redirect; removed it", {
|
|
149
|
+
storyId,
|
|
150
|
+
path: target,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
logger?.error("sandbox", "A sandboxed command created a git commondir redirect that could not be removed", {
|
|
155
|
+
storyId,
|
|
156
|
+
path: target,
|
|
157
|
+
error: errorMessage(err),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { quoteArgvForShell } from "./argv-quote.ts";
|
|
2
|
+
export { _gitGuardDeps, listGitGuardFiles, strayCommonDirTripwire } from "./git-guards.ts";
|
|
3
|
+
export { _launcherDeps, type CommandLauncherOptions, createCommandLauncher, DISABLED_SANDBOX_STATE, } from "./launcher.ts";
|
|
4
|
+
export { denialHintLine, LIKELY_SANDBOX_DENIAL, rawBashRefusalReason, sandboxSentence, unsandboxedSentence, } from "./messages.ts";
|
|
5
|
+
export { buildSandboxPolicy, type SandboxPolicyInput } from "./policy-builder.ts";
|
|
6
|
+
export { _policyInputDeps, defaultTempRoots, type GitLayout, listCredentialFiles, listNaxEntries, resolveGitLayout, runTempRoots, } from "./policy-inputs.ts";
|
|
7
|
+
export { _probeDeps, probeSandbox } from "./probe.ts";
|
|
8
|
+
export { _resetSandboxRegistryForTests, _sandboxRegistryDeps, probeSandboxOnce, resetSandboxBackend, sandboxBackendFor, warnSandboxUnavailableOnce, } from "./registry.ts";
|
|
9
|
+
export { _sessionTmpDeps, runTmpRoot, sessionTmpDir, sessionTmpDirUnder } from "./session-tmp.ts";
|
|
10
|
+
export { _srtBackendDeps, createSrtBackend } from "./srt-backend.ts";
|
|
11
|
+
export type { CommandLauncher, LaunchRequest, LaunchResult, LaunchSpec, ProbeResult, SandboxBackend, SandboxBackendName, SandboxNetworkPolicy, SandboxPolicy, SandboxRecord, SandboxState, SandboxWrapRequest, } from "./types.ts";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { quoteArgvForShell } from "./argv-quote.js";
|
|
2
|
+
export { _gitGuardDeps, listGitGuardFiles, strayCommonDirTripwire } from "./git-guards.js";
|
|
3
|
+
export { _launcherDeps, createCommandLauncher, DISABLED_SANDBOX_STATE, } from "./launcher.js";
|
|
4
|
+
export { denialHintLine, LIKELY_SANDBOX_DENIAL, rawBashRefusalReason, sandboxSentence, unsandboxedSentence, } from "./messages.js";
|
|
5
|
+
export { buildSandboxPolicy } from "./policy-builder.js";
|
|
6
|
+
export { _policyInputDeps, defaultTempRoots, listCredentialFiles, listNaxEntries, resolveGitLayout, runTempRoots, } from "./policy-inputs.js";
|
|
7
|
+
export { _probeDeps, probeSandbox } from "./probe.js";
|
|
8
|
+
export { _resetSandboxRegistryForTests, _sandboxRegistryDeps, probeSandboxOnce, resetSandboxBackend, sandboxBackendFor, warnSandboxUnavailableOnce, } from "./registry.js";
|
|
9
|
+
export { _sessionTmpDeps, runTmpRoot, sessionTmpDir, sessionTmpDirUnder } from "./session-tmp.js";
|
|
10
|
+
export { _srtBackendDeps, createSrtBackend } from "./srt-backend.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runArgv } from "#src/internal/argv-exec";
|
|
2
|
+
import type { CommandLauncher, SandboxBackend, SandboxPolicy, SandboxState } from "./types.ts";
|
|
3
|
+
export declare const DISABLED_SANDBOX_STATE: SandboxState;
|
|
4
|
+
export declare const _launcherDeps: {
|
|
5
|
+
runArgv: typeof runArgv;
|
|
6
|
+
newCommandId: () => string;
|
|
7
|
+
mkdir: (path: string) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export interface CommandLauncherOptions {
|
|
10
|
+
readonly state: SandboxState;
|
|
11
|
+
readonly backend?: SandboxBackend;
|
|
12
|
+
/**
|
|
13
|
+
* Build the policy for one command.
|
|
14
|
+
*
|
|
15
|
+
* `tmpDirInForce` says whether THIS command gets the session's own `TMPDIR`
|
|
16
|
+
* override: the launcher declared a `tmpDir` and the per-run `ensureTmpDir`
|
|
17
|
+
* recreated it, so `runWrapped` will apply the `export TMPDIR=…` prefix. It is
|
|
18
|
+
* false when the override is dropped, and srt's own forced `TMPDIR` applies
|
|
19
|
+
* instead — a builder that denies that directory would then hand the session a
|
|
20
|
+
* `TMPDIR` its own sandbox refuses to write (#2301, SPEC-tmp-confinement.md:132).
|
|
21
|
+
*
|
|
22
|
+
* Reported separately from `state` on purpose: `state.sharedTmp === false` is
|
|
23
|
+
* the construction-time answer (did the session RESOLVE as confined) and never
|
|
24
|
+
* changes, while this is re-decided on every run. A session dir that recreates
|
|
25
|
+
* for one run and not the next is confined by the first and not the second.
|
|
26
|
+
*/
|
|
27
|
+
readonly policyFor?: (root: string, tmpDirInForce: boolean) => Promise<SandboxPolicy>;
|
|
28
|
+
/** Runs after every wrapped command, before control returns to nax (e.g. a git tripwire, #2198). */
|
|
29
|
+
readonly afterWrapped?: () => Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* US-004 — the session's temp directory. Created before every run, then handed
|
|
32
|
+
* to the child as `TMPDIR`/`TMP`/`TEMP` so stray files land under the run's own
|
|
33
|
+
* root instead of the shared `/tmp`.
|
|
34
|
+
*/
|
|
35
|
+
readonly tmpDir?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function createCommandLauncher(opts: CommandLauncherOptions): CommandLauncher;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an agent-authored command runs (spec 5.1). Never WHETHER -- that is the
|
|
3
|
+
* policy's call (single-gate rule). Handed only to Bash and RunCommand's Exec
|
|
4
|
+
* branch, so a user-authored command can never be wrapped (D14).
|
|
5
|
+
*
|
|
6
|
+
* Two rules with teeth:
|
|
7
|
+
* - F6: the backend returns argv only; runArgv receives the CALLER's env
|
|
8
|
+
* overlay and strips from process.env exactly as today.
|
|
9
|
+
* - A wrap that throws is an error. The command never runs unwrapped: what
|
|
10
|
+
* stops is the command, never the sandbox.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { mkdir } from "node:fs/promises";
|
|
14
|
+
import { errorMessage } from "#src/infra/errors";
|
|
15
|
+
import { getSafeLogger, NaxError } from "#src/infra/index";
|
|
16
|
+
import { runArgv } from "#src/internal/argv-exec";
|
|
17
|
+
import { quoteArgvForShell } from "./argv-quote.js";
|
|
18
|
+
import { denialHintLine, LIKELY_SANDBOX_DENIAL } from "./messages.js";
|
|
19
|
+
export const DISABLED_SANDBOX_STATE = { kind: "disabled" };
|
|
20
|
+
export const _launcherDeps = {
|
|
21
|
+
runArgv,
|
|
22
|
+
newCommandId: () => randomUUID(),
|
|
23
|
+
mkdir: (path) => mkdir(path, { recursive: true }).then(() => undefined),
|
|
24
|
+
};
|
|
25
|
+
function logicalArgv(req) {
|
|
26
|
+
return req.spec.kind === "shell" ? [req.spec.shell, "-c", req.spec.command] : req.spec.argv;
|
|
27
|
+
}
|
|
28
|
+
/** POSIX single-quoting: wrap in `'`, turning an embedded `'` into `'\''`. */
|
|
29
|
+
function shellSingleQuote(value) {
|
|
30
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
31
|
+
}
|
|
32
|
+
/** `export TMPDIR=… TMP=… TEMP=…; ` — the prefix a wrapped command carries, since srt replaces the child env. */
|
|
33
|
+
function tmpEnvPrefix(tmpDir) {
|
|
34
|
+
const q = shellSingleQuote(tmpDir);
|
|
35
|
+
return `export TMPDIR=${q} TMP=${q} TEMP=${q}; `;
|
|
36
|
+
}
|
|
37
|
+
/** The TMPDIR/TMP/TEMP overlay for an unwrapped run; the request's own env wins per key. */
|
|
38
|
+
function withTmpEnv(req, tmpDir) {
|
|
39
|
+
if (tmpDir === undefined)
|
|
40
|
+
return req.env;
|
|
41
|
+
return { TMPDIR: tmpDir, TMP: tmpDir, TEMP: tmpDir, ...(req.env ?? {}) };
|
|
42
|
+
}
|
|
43
|
+
async function ensureTmpDir(tmpDir) {
|
|
44
|
+
try {
|
|
45
|
+
await _launcherDeps.mkdir(tmpDir);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
getSafeLogger()?.warn("sandbox", "could not create session temp dir — running without TMPDIR override", {
|
|
50
|
+
tmpDir,
|
|
51
|
+
error: errorMessage(err),
|
|
52
|
+
});
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function runUnwrapped(req, sandbox, env) {
|
|
57
|
+
const argv = logicalArgv(req);
|
|
58
|
+
const overlay = env.tmpDir !== undefined ? withTmpEnv(req, env.tmpDir) : req.env;
|
|
59
|
+
const result = await _launcherDeps.runArgv({
|
|
60
|
+
argv,
|
|
61
|
+
cwd: req.cwd,
|
|
62
|
+
timeoutMs: req.timeoutMs,
|
|
63
|
+
stripEnvVars: [...req.stripEnvVars],
|
|
64
|
+
...(overlay !== undefined ? { env: overlay } : {}),
|
|
65
|
+
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
66
|
+
});
|
|
67
|
+
return { ...result, executed: argv, sandbox };
|
|
68
|
+
}
|
|
69
|
+
async function runWrapped(req, backend, policy, env) {
|
|
70
|
+
const rawCommand = req.spec.kind === "shell" ? req.spec.command : quoteArgvForShell(req.spec.argv);
|
|
71
|
+
// srt replaces the child environment wholesale, so a TMPDIR override cannot
|
|
72
|
+
// ride an `env` overlay — it has to be part of the shell command itself.
|
|
73
|
+
// Other overlay keys DO survive: the live suite proves it (`AGENT=1` reaches
|
|
74
|
+
// a wrapped child — see test/integration/sandbox/sandbox-live.test.ts
|
|
75
|
+
// "US-004 — the AGENT=1 overlay reaches a wrapped child"). `executed` below
|
|
76
|
+
// stays the unprefixed logical argv, so the ledger records what the agent
|
|
77
|
+
// wrote, not this shim.
|
|
78
|
+
const command = env.tmpDir !== undefined ? `${tmpEnvPrefix(env.tmpDir)}${rawCommand}` : rawCommand;
|
|
79
|
+
const shell = req.spec.kind === "shell" ? req.spec.shell : "/bin/sh";
|
|
80
|
+
const commandId = _launcherDeps.newCommandId();
|
|
81
|
+
let argv;
|
|
82
|
+
try {
|
|
83
|
+
argv = await backend.wrap({ command, shell, policy, cwd: req.cwd, commandId });
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new NaxError(`[sandbox] could not wrap the command: ${errorMessage(err)}`, "SANDBOX_WRAP_FAILED", {
|
|
87
|
+
stage: "sandbox",
|
|
88
|
+
backend: backend.name,
|
|
89
|
+
commandId,
|
|
90
|
+
cause: err,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const result = await _launcherDeps.runArgv({
|
|
95
|
+
argv,
|
|
96
|
+
cwd: req.cwd,
|
|
97
|
+
timeoutMs: req.timeoutMs,
|
|
98
|
+
stripEnvVars: [...req.stripEnvVars],
|
|
99
|
+
...(req.env !== undefined ? { env: req.env } : {}),
|
|
100
|
+
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
101
|
+
});
|
|
102
|
+
const denied = result.exitCode !== 0 && LIKELY_SANDBOX_DENIAL.test(result.stderr);
|
|
103
|
+
const violations = backend.annotate(commandId, result.stderr);
|
|
104
|
+
const extra = [
|
|
105
|
+
...(denied ? [denialHintLine(policy.writeRoots, env.sharedTmp)] : []),
|
|
106
|
+
...(violations !== "" ? [violations] : []),
|
|
107
|
+
];
|
|
108
|
+
return {
|
|
109
|
+
...result,
|
|
110
|
+
stderr: extra.length > 0 ? `${result.stderr}\n${extra.join("\n")}` : result.stderr,
|
|
111
|
+
executed: logicalArgv(req),
|
|
112
|
+
sandbox: {
|
|
113
|
+
backend: backend.name,
|
|
114
|
+
wrapped: true,
|
|
115
|
+
argv,
|
|
116
|
+
...(denied ? { denialHint: true } : {}),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
backend.commandFinished();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function createCommandLauncher(opts) {
|
|
125
|
+
const { state } = opts;
|
|
126
|
+
const env = {
|
|
127
|
+
tmpDir: opts.tmpDir,
|
|
128
|
+
// A confined session's denial hint points at $TMPDIR instead of /tmp; a
|
|
129
|
+
// shared-temp session's hint must not contradict the policy it runs under.
|
|
130
|
+
sharedTmp: state.kind !== "available" || state.sharedTmp !== false,
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
state,
|
|
134
|
+
async run(req) {
|
|
135
|
+
const tmpDir = env.tmpDir !== undefined && (await ensureTmpDir(env.tmpDir)) ? env.tmpDir : undefined;
|
|
136
|
+
if (state.kind === "disabled")
|
|
137
|
+
return runUnwrapped(req, { backend: "none", wrapped: false }, { ...env, tmpDir });
|
|
138
|
+
if (state.kind === "unavailable") {
|
|
139
|
+
return runUnwrapped(req, { backend: state.backend, wrapped: false, reason: state.reason }, { ...env, tmpDir });
|
|
140
|
+
}
|
|
141
|
+
if (opts.backend === undefined || opts.policyFor === undefined) {
|
|
142
|
+
throw new NaxError("[sandbox] an available launcher needs a backend and a policy", "SANDBOX_NOT_CONFIGURED", {
|
|
143
|
+
stage: "sandbox",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
// `runWrapped` is handed this same `tmpDir` and applies the
|
|
148
|
+
// `export TMPDIR=…` prefix only when it is defined, so `tmpDir !== undefined`
|
|
149
|
+
// is exactly the "the override is in force for this command" fact.
|
|
150
|
+
return await runWrapped(req, opts.backend, await opts.policyFor(req.root, tmpDir !== undefined), {
|
|
151
|
+
...env,
|
|
152
|
+
tmpDir,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await opts.afterWrapped?.();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every sentence the agent reads about the sandbox (spec 5.5), in one place so
|
|
3
|
+
* descriptions, refusals and result notes cannot drift apart. The agent must
|
|
4
|
+
* always know whether it is sandboxed and what it may write (spec S5).
|
|
5
|
+
*/
|
|
6
|
+
export declare const LIKELY_SANDBOX_DENIAL: RegExp;
|
|
7
|
+
export declare function sandboxSentence(network: "open" | readonly string[], sharedTmp?: boolean): string;
|
|
8
|
+
export declare function unsandboxedSentence(reason: string): string;
|
|
9
|
+
export declare function rawBashRefusalReason(reason: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* US-003: the denial path is exactly where an agent discovers the sandbox
|
|
12
|
+
* refuses a `/tmp` write, so the hint names the two roots that do work -- the
|
|
13
|
+
* session temp dir and the scratchpad -- rather than only listing the policy's
|
|
14
|
+
* write roots. A shared-temp session (`allowSharedTmp: true`) can write `/tmp`,
|
|
15
|
+
* so its hint omits the "not /tmp" clause that would contradict its policy.
|
|
16
|
+
*/
|
|
17
|
+
export declare function denialHintLine(writeRoots: readonly string[], sharedTmp?: boolean): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every sentence the agent reads about the sandbox (spec 5.5), in one place so
|
|
3
|
+
* descriptions, refusals and result notes cannot drift apart. The agent must
|
|
4
|
+
* always know whether it is sandboxed and what it may write (spec S5).
|
|
5
|
+
*/
|
|
6
|
+
export const LIKELY_SANDBOX_DENIAL = /Operation not permitted|Read-only file system/;
|
|
7
|
+
function describeNetwork(network) {
|
|
8
|
+
if (network === "open")
|
|
9
|
+
return "network access is unrestricted";
|
|
10
|
+
if (network.length === 0)
|
|
11
|
+
return "network access is disabled";
|
|
12
|
+
return `network access is limited to ${network.join(", ")}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* US-003: the writable temp root the sentence advertises. A confined session
|
|
16
|
+
* (`sharedTmp: false`) can only write under the run's own temp directory, so
|
|
17
|
+
* promising "the system temp directories" would send the agent at a denied
|
|
18
|
+
* write; a shared-temp session keeps the wording it had before.
|
|
19
|
+
*/
|
|
20
|
+
const tempRootsPhrase = (sharedTmp) => sharedTmp ? "the system temp directories" : "this run's temp directory ($TMPDIR)";
|
|
21
|
+
export function sandboxSentence(network, sharedTmp = true) {
|
|
22
|
+
return (`inside an OS sandbox: writes are allowed only under the repository root, ${tempRootsPhrase(sharedTmp)} and ` +
|
|
23
|
+
"package-manager caches -- and nothing under .nax/ except .nax/scratchpad/, since .nax/ is nax's own state " +
|
|
24
|
+
"(change a feature's acceptance test with the Edit tool); credential files (~/.ssh, ~/.aws, ~/.npmrc, nax credentials and similar) are unreadable; " +
|
|
25
|
+
`${describeNetwork(network)}. A write anywhere else fails with "Operation not permitted" or "Read-only file system" ` +
|
|
26
|
+
"-- that is the sandbox, not a bug in your command.");
|
|
27
|
+
}
|
|
28
|
+
export function unsandboxedSentence(reason) {
|
|
29
|
+
return `Commands are NOT sandboxed on this machine (${reason}).`;
|
|
30
|
+
}
|
|
31
|
+
export function rawBashRefusalReason(reason) {
|
|
32
|
+
return (`sandbox unavailable (${reason}): raw bash requires the sandbox when execution.sandbox.enabled is true -- ` +
|
|
33
|
+
"set this stage's bashApproval to gated or escalate, or disable the sandbox.");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* US-003: the denial path is exactly where an agent discovers the sandbox
|
|
37
|
+
* refuses a `/tmp` write, so the hint names the two roots that do work -- the
|
|
38
|
+
* session temp dir and the scratchpad -- rather than only listing the policy's
|
|
39
|
+
* write roots. A shared-temp session (`allowSharedTmp: true`) can write `/tmp`,
|
|
40
|
+
* so its hint omits the "not /tmp" clause that would contradict its policy.
|
|
41
|
+
*/
|
|
42
|
+
export function denialHintLine(writeRoots, sharedTmp = false) {
|
|
43
|
+
const tempClause = sharedTmp ? "" : " For temporary files use $TMPDIR or .nax/scratchpad/, not /tmp.";
|
|
44
|
+
return (`note: this command ran in the nax sandbox; that failure may be a sandbox denial -- writable roots: ${writeRoots.join(", ")}.` +
|
|
45
|
+
tempClause);
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type SandboxConfig } from "../config/schemas-sandbox.ts";
|
|
2
|
+
import type { GitLayout } from "./policy-inputs.ts";
|
|
3
|
+
import type { SandboxPolicy } from "./types.ts";
|
|
4
|
+
export interface SandboxPolicyInput {
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly git: GitLayout;
|
|
7
|
+
/** Redirecting git files present now, every worktree included (listGitGuardFiles, #2198). */
|
|
8
|
+
readonly gitGuardFiles: readonly string[];
|
|
9
|
+
/** Top-level entry names under `<root>/.nax` right now (listNaxEntries). */
|
|
10
|
+
readonly naxEntries: readonly string[];
|
|
11
|
+
readonly credentialFiles: readonly string[];
|
|
12
|
+
readonly approvalsFile?: string;
|
|
13
|
+
/** US-006 — the trust store (`trustStorePath()`), denied so a command cannot rewrite its own trust. */
|
|
14
|
+
readonly trustStoreFile?: string;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
readonly tempRoots: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* #2301: whether THIS command's temp writes stay confined — the RESOLVE-time
|
|
19
|
+
* answer ANDed with the fact that this launch's `TMPDIR` override is in force.
|
|
20
|
+
*
|
|
21
|
+
* It governs srt's forced TMPDIR on BOTH sides, not only the deny. On darwin,
|
|
22
|
+
* `true` drops `/tmp/claude` from `writeRoots` and adds both of its spellings
|
|
23
|
+
* to `denyWrite`; `false` does the reverse, so a command that did not confine
|
|
24
|
+
* keeps `/tmp/claude` writable as a plain grant. `input.tempRoots` is passed
|
|
25
|
+
* through either way: the flag moves srt's `/tmp/claude` grant, never the
|
|
26
|
+
* session's own temp roots.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately NOT the value behind `SandboxState.sharedTmp === false` or
|
|
29
|
+
* `isTempConfined`. Those are the resolve-time answer, fixed when the launcher
|
|
30
|
+
* is built, while this is re-decided on every launch by ANDing that answer with
|
|
31
|
+
* whether the session temp dir was re-created. The two are MEANT to disagree: a
|
|
32
|
+
* session whose dir fails to re-create keeps `sharedTmp: false` and
|
|
33
|
+
* `isTempConfined() === true` while this reads `false` for that one command. Do
|
|
34
|
+
* not reconcile them by making either one follow the other.
|
|
35
|
+
*
|
|
36
|
+
* Absent means it confined neither way, so every construction outside
|
|
37
|
+
* `resolveSessionSandbox` keeps today's behaviour; the only other builder in
|
|
38
|
+
* the tree, the probe, writes its policy as a literal and never reaches here.
|
|
39
|
+
*/
|
|
40
|
+
readonly confined?: boolean;
|
|
41
|
+
readonly platform: NodeJS.Platform;
|
|
42
|
+
readonly config: SandboxConfig;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildSandboxPolicy(input: SandboxPolicyInput): SandboxPolicy;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure: everything the sandbox allows and denies for one command (spec 5.3).
|
|
3
|
+
*
|
|
4
|
+
* Rebuilt per call so a `.nax/` entry created mid-run gets its deny (nax#2260).
|
|
5
|
+
* Every emitted path goes through realOrRaw -- srt realpaths only paths
|
|
6
|
+
* that exist, so a deny for a not-yet-created file under a symlinked temp dir
|
|
7
|
+
* would otherwise keep a spelling the kernel never sees (spec 12, finding 3).
|
|
8
|
+
*/
|
|
9
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { NaxError } from "#src/infra/index";
|
|
11
|
+
import { realOrRaw } from "#src/internal/realpath";
|
|
12
|
+
import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
|
|
13
|
+
import { NAX_ALWAYS_DENIED_ENTRIES, NAX_SCRATCHPAD_ENTRY, naxWriteOptIns, QUEUE_CONTROL_FILES, } from "../tools/nax-owned-writes.js";
|
|
14
|
+
import { BUILTIN_CACHE_WRITE_ROOTS, BUILTIN_CREDENTIAL_READ_DENIES, MACOS_CACHE_WRITE_ROOT, SRT_MACOS_TMPDIR, SRT_MACOS_TMPDIR_DENIES, } from "./defaults.js";
|
|
15
|
+
import { WORKTREE_COMMON_WRITE_DIRS, WORKTREE_CONFIG_FILE } from "./git-guards.js";
|
|
16
|
+
function expandHome(p, home) {
|
|
17
|
+
if (p === "~")
|
|
18
|
+
return home;
|
|
19
|
+
return p.startsWith("~/") ? join(home, p.slice(2)) : p;
|
|
20
|
+
}
|
|
21
|
+
/** Throws on a glob: a Linux backend would drop it silently (F1), so fail closed instead. */
|
|
22
|
+
function literal(paths) {
|
|
23
|
+
const resolved = [...new Set(paths.map((p) => realOrRaw(p)))];
|
|
24
|
+
const glob = resolved.find((p) => SANDBOX_GLOB_CHARS.test(p));
|
|
25
|
+
if (glob !== undefined) {
|
|
26
|
+
throw new NaxError(`[sandbox] policy path is not literal (glob character): ${glob}`, "SANDBOX_POLICY_NOT_LITERAL", {
|
|
27
|
+
stage: "sandbox",
|
|
28
|
+
path: glob,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
function gitDenies(root, git) {
|
|
34
|
+
if (git.kind === "none")
|
|
35
|
+
return [];
|
|
36
|
+
const common = git.kind === "worktree" ? git.commonDir : git.gitDir;
|
|
37
|
+
// config.worktree is read when extensions.worktreeConfig is on; an absent one
|
|
38
|
+
// is safe to deny (srt stubs it empty, and empty is valid config) (#2198).
|
|
39
|
+
const shared = [join(common, "hooks"), join(common, "config"), join(common, WORKTREE_CONFIG_FILE)];
|
|
40
|
+
if (git.kind === "main")
|
|
41
|
+
return shared;
|
|
42
|
+
// A worktree's `.git` is a pointer FILE, and gitdir/commondir point back;
|
|
43
|
+
// repointing any of them at an agent-written config (core.hooksPath) would
|
|
44
|
+
// make nax's own unsandboxed git run hooks (spec 12, finding 4).
|
|
45
|
+
return [
|
|
46
|
+
...shared,
|
|
47
|
+
join(root, ".git"),
|
|
48
|
+
join(git.gitDir, "gitdir"),
|
|
49
|
+
join(git.gitDir, "commondir"),
|
|
50
|
+
join(git.gitDir, WORKTREE_CONFIG_FILE),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A worktree's own admin dir plus the shared subdirs its git writes -- never
|
|
55
|
+
* the whole common dir, whose top-level `commondir` would otherwise be a live
|
|
56
|
+
* redirect for concurrent unsandboxed git while the command runs (#2211).
|
|
57
|
+
* srt skips a write root that does not exist yet.
|
|
58
|
+
*/
|
|
59
|
+
function worktreeGitWriteRoots(git) {
|
|
60
|
+
if (git.kind !== "worktree")
|
|
61
|
+
return [];
|
|
62
|
+
return [git.gitDir, ...WORKTREE_COMMON_WRITE_DIRS.map((dir) => join(git.commonDir, dir))];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* nax#2260: every top-level `.nax/` entry, whole, plus the ones nax reads even
|
|
66
|
+
* when absent -- except the scratchpad and entries opted in via allowWrite.
|
|
67
|
+
* srt applies deny over allow and on Linux turns every literal deny into a
|
|
68
|
+
* bind mount, so whole entries (one `features` deny, not one per PRD) keep
|
|
69
|
+
* both the protection and the per-command cost bounded.
|
|
70
|
+
*
|
|
71
|
+
* Only `<root>/.nax` is covered. A monorepo package's own `.nax/`
|
|
72
|
+
* (`packages/<pkg>/.nax/`) is deliberately excluded: what lives there --
|
|
73
|
+
* cache/, scratchpad/, status.json, the features' acceptance tests and run
|
|
74
|
+
* artifacts -- is gitignored and rebuilt or regenerated, so deleting it is
|
|
75
|
+
* harmless, and the package's config sits under the root's protected `mono/`.
|
|
76
|
+
* The exception is an optional `packages/<pkg>/.nax/rules/`, which nax loads
|
|
77
|
+
* into prompts; covering it needs package discovery, because a `.nax/` at any
|
|
78
|
+
* depth also matches test-fixture projects (`test/fixtures/<p>/.nax/`).
|
|
79
|
+
*/
|
|
80
|
+
function naxDenies(root, entries, allowWrite) {
|
|
81
|
+
const optIns = naxWriteOptIns(root, allowWrite);
|
|
82
|
+
return [...new Set([...entries, ...NAX_ALWAYS_DENIED_ENTRIES])]
|
|
83
|
+
.filter((name) => name !== NAX_SCRATCHPAD_ENTRY && !optIns.has(name))
|
|
84
|
+
.map((name) => join(root, ".nax", name));
|
|
85
|
+
}
|
|
86
|
+
export function buildSandboxPolicy(input) {
|
|
87
|
+
const { root, home, config } = input;
|
|
88
|
+
const darwin = input.platform === "darwin";
|
|
89
|
+
// #2301: a CONFINED session does not need srt's forced TMPDIR. srt always
|
|
90
|
+
// allows `/tmp/claude` (`SANDBOX_OWN_WRITE_PATHS`) and hands the child a
|
|
91
|
+
// `TMPDIR` of `/tmp/claude` (sandbox-utils.js:630), and in a macOS Seatbelt
|
|
92
|
+
// profile it renders the allow rules before the deny rules, where the later deny
|
|
93
|
+
// wins — so the only way to take the write back is an explicit deny, which is
|
|
94
|
+
// what a confined session gets.
|
|
95
|
+
//
|
|
96
|
+
// Both spellings, because `realOrRaw` resolves the nearest EXISTING ancestor and
|
|
97
|
+
// `/tmp` exists: on a stock macOS host `/tmp` is a symlink to `/private/tmp`, so
|
|
98
|
+
// `literal()` collapses the pair to one entry and it costs nothing there. The
|
|
99
|
+
// pair still matters on a host where `/tmp` is a real directory, or a symlink
|
|
100
|
+
// somewhere other than `/private/tmp`: there the two resolve apart and a single
|
|
101
|
+
// spelling would leave srt's other one writable.
|
|
102
|
+
//
|
|
103
|
+
// Nothing needs the grant instead: a session is confined only when it HAS a temp
|
|
104
|
+
// dir, and `runWrapped` prefixes the command with
|
|
105
|
+
// `export TMPDIR=<that dir> TMP=… TEMP=…` (src/sandbox/launcher.ts:138),
|
|
106
|
+
// replacing srt's value before the agent's command runs. That prefix rides on
|
|
107
|
+
// `createCommandLauncher`'s per-run mkdir (src/sandbox/launcher.ts:194), so it is
|
|
108
|
+
// conditional — when the mkdir fails the override is dropped and the agent's
|
|
109
|
+
// `TMPDIR` is srt's `/tmp/claude` again, which is why that case needs deciding
|
|
110
|
+
// before a deny can be relied on.
|
|
111
|
+
//
|
|
112
|
+
// A session that did NOT confine keeps the grant: its temp roots are
|
|
113
|
+
// `defaultTempRoots`, which allows `/tmp` outright, so `/tmp/claude` is inside an
|
|
114
|
+
// allowed root anyway and a deny would contradict the session's own denial hint
|
|
115
|
+
// (`denialHintLine`).
|
|
116
|
+
//
|
|
117
|
+
// darwin only, deliberately: srt's `SANDBOX_OWN_WRITE_PATHS` is
|
|
118
|
+
// platform-independent, so a confined LINUX session still gets `/tmp/claude`.
|
|
119
|
+
// Left open on purpose — on bwrap a literal deny becomes a bind mount, a
|
|
120
|
+
// different mechanism that needs its own check before a deny is added there.
|
|
121
|
+
const confined = input.confined === true;
|
|
122
|
+
const writeRoots = literal([
|
|
123
|
+
root,
|
|
124
|
+
...worktreeGitWriteRoots(input.git),
|
|
125
|
+
...input.tempRoots,
|
|
126
|
+
...(darwin ? [...(confined ? [] : [SRT_MACOS_TMPDIR]), join(home, MACOS_CACHE_WRITE_ROOT)] : []),
|
|
127
|
+
...BUILTIN_CACHE_WRITE_ROOTS.map((rel) => join(home, rel)),
|
|
128
|
+
...config.filesystem.allowWrite.map((p) => {
|
|
129
|
+
const expanded = expandHome(p, home);
|
|
130
|
+
return isAbsolute(expanded) ? expanded : resolve(root, expanded);
|
|
131
|
+
}),
|
|
132
|
+
]);
|
|
133
|
+
const denyWrite = literal([
|
|
134
|
+
...naxDenies(root, input.naxEntries, config.filesystem.allowWrite),
|
|
135
|
+
...[...QUEUE_CONTROL_FILES].map((name) => join(root, name)),
|
|
136
|
+
...gitDenies(root, input.git),
|
|
137
|
+
...input.gitGuardFiles,
|
|
138
|
+
...(input.approvalsFile !== undefined ? [input.approvalsFile] : []),
|
|
139
|
+
...(input.trustStoreFile !== undefined ? [input.trustStoreFile] : []),
|
|
140
|
+
...(darwin && confined ? SRT_MACOS_TMPDIR_DENIES : []),
|
|
141
|
+
]);
|
|
142
|
+
const denyRead = literal([
|
|
143
|
+
...BUILTIN_CREDENTIAL_READ_DENIES.map((rel) => join(home, rel)),
|
|
144
|
+
...input.credentialFiles,
|
|
145
|
+
...config.filesystem.denyRead.map((p) => resolve(root, expandHome(p, home))),
|
|
146
|
+
]);
|
|
147
|
+
const allowed = config.network.allowedDomains;
|
|
148
|
+
return { writeRoots, denyWrite, denyRead, network: allowed === undefined ? {} : { allowedDomains: [...allowed] } };
|
|
149
|
+
}
|