@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete a file whose path the policy already resolved and approved.
|
|
3
|
+
*
|
|
4
|
+
* nax#1972: this used to be tracked-only, on the theory that a tracked
|
|
5
|
+
* file's content is in git history (an undo away) while an untracked file
|
|
6
|
+
* exists only on disk and is therefore unrecoverable. That rationale is
|
|
7
|
+
* retired -- it made the guard unsatisfiable for a file the agent itself
|
|
8
|
+
* wrote this session, which is untracked by construction. Paired with the
|
|
9
|
+
* #1937 denial redirect (Exec `rm` bounces back to Delete), the agent had no
|
|
10
|
+
* legal way to remove its own scratch output, and its fallback was to blank
|
|
11
|
+
* files to 0 bytes with Write -- which the auto-commit sweep then carried
|
|
12
|
+
* into history anyway. Refusing the deletion did not make anything more
|
|
13
|
+
* recoverable; it just moved the damage to a different tool.
|
|
14
|
+
*
|
|
15
|
+
* The new rule follows the three states git actually puts a path in:
|
|
16
|
+
*
|
|
17
|
+
* - tracked -> allow (unchanged)
|
|
18
|
+
* - untracked AND gitignored -> refuse (unchanged verdict, new reason)
|
|
19
|
+
* - untracked and NOT gitignored -> allow (the fix)
|
|
20
|
+
*
|
|
21
|
+
* Both untracked classes are equally unrecoverable -- neither is in git
|
|
22
|
+
* history -- so recoverability alone no longer explains the split. What
|
|
23
|
+
* distinguishes them is declared intent. A gitignored path has a rule
|
|
24
|
+
* somebody wrote on purpose, saying "this lives only on this machine"
|
|
25
|
+
* (`.env`, `.nax-pids`, `.claude/settings.local.json`, the fragments and
|
|
26
|
+
* plan-log directories under `.nax` are real examples in this repo) --
|
|
27
|
+
* deleting it is
|
|
28
|
+
* unrecoverable AND was never meant to be sent anywhere else, so the refusal
|
|
29
|
+
* stays, but the message now says why instead of telling the agent to commit
|
|
30
|
+
* a file its own author chose to keep out of git. An untracked-and-not-
|
|
31
|
+
* ignored path has no such rule attached to it at all: nobody has said
|
|
32
|
+
* anything about it one way or the other. In practice that is agent scratch
|
|
33
|
+
* or work in progress, so it is allowed.
|
|
34
|
+
*
|
|
35
|
+
* Accepted cost: a genuine new source file the agent wrote and has not yet
|
|
36
|
+
* committed is untracked-and-not-ignored too, so this permission also makes
|
|
37
|
+
* IT deletable -- there is no signal in git alone that separates "my own
|
|
38
|
+
* throwaway probe" from "a real file I just haven't committed yet". That is
|
|
39
|
+
* deliberate, not an oversight: it is the same permission that lets an agent
|
|
40
|
+
* clean up after itself, and the completion-phase auto-commit sweep
|
|
41
|
+
* (`autoCommitIfDirty`, `src/utils/git.ts`) is the backstop if something
|
|
42
|
+
* real gets removed before it was staged.
|
|
43
|
+
*
|
|
44
|
+
* `.git/` never reaches this point at all -- `resolveWithin`
|
|
45
|
+
* (`src/tools/policy.ts`) excludes it for every path-bearing tool, so
|
|
46
|
+
* `ctx.resolvedPaths` cannot contain a `.git/` path in the first place
|
|
47
|
+
* (nax#1943). Re-checking it here would be exactly the per-tool
|
|
48
|
+
* re-remembering that issue exists to prevent.
|
|
49
|
+
*
|
|
50
|
+
* Not `git rm`: gitTool documents a deliberate read/write split, and `git rm`
|
|
51
|
+
* both deletes and stages, folding two capabilities into one call. Plain
|
|
52
|
+
* unlink plus the existing GitCommit keeps each seam doing one thing --
|
|
53
|
+
* `git add -- <deleted path>` stages a deletion, so no change to GitCommit is
|
|
54
|
+
* needed.
|
|
55
|
+
*/
|
|
56
|
+
import { lstat, unlink } from "node:fs/promises";
|
|
57
|
+
import { relative, resolve, sep } from "node:path";
|
|
58
|
+
import { gitWithTimeout } from "#src/internal/git-exec";
|
|
59
|
+
import { matchesDenyPaths } from "./deny-paths.js";
|
|
60
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
61
|
+
export const deleteTool = {
|
|
62
|
+
name: "Delete",
|
|
63
|
+
description: "Delete a file. A tracked file is always removable. An untracked file is removable unless it is gitignored -- a gitignored path was deliberately kept out of git, so removing it is unrecoverable and is refused. Directories are refused. The removal still has to be staged -- pass the same path to GitCommit afterwards.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
path: { type: "string", description: "Path relative to the repository root" },
|
|
68
|
+
},
|
|
69
|
+
required: ["path"],
|
|
70
|
+
},
|
|
71
|
+
scope: { pathFields: ["path"] },
|
|
72
|
+
async run(input, ctx) {
|
|
73
|
+
if (ctx.resolvedPaths[0] === undefined)
|
|
74
|
+
return { content: "no path supplied", isError: true };
|
|
75
|
+
const shown = String(input.path);
|
|
76
|
+
// The policy uses a realpath to prove containment, which dereferences a
|
|
77
|
+
// symlink. Deletion must instead act on the requested directory entry: a
|
|
78
|
+
// tracked symlink is itself recoverable, while its target may be a
|
|
79
|
+
// different tracked file. The policy has already validated `shown`.
|
|
80
|
+
const target = resolve(ctx.root, shown);
|
|
81
|
+
// denyPaths (nax#1972) is a repo-authored narrowing of Delete, and is
|
|
82
|
+
// checked before anything else this function decides -- including
|
|
83
|
+
// existence -- because it is a blanket refusal on the path itself, not a
|
|
84
|
+
// judgment about the path's current state on disk.
|
|
85
|
+
//
|
|
86
|
+
// Matched against the CANONICAL entry, never the raw `input.path`. The
|
|
87
|
+
// policy resolves before it approves, so a denylist consulted on the
|
|
88
|
+
// caller's own spelling is a different parser than the one that granted
|
|
89
|
+
// the call -- and every alternate spelling of the same file ("./x",
|
|
90
|
+
// "x//y", "a/../x", an absolute path) walks straight past it. Deriving
|
|
91
|
+
// the relative form from `target` collapses all of them to one string.
|
|
92
|
+
// Deliberately NOT realpath: a symlink's entry is what Delete removes,
|
|
93
|
+
// and removing a link cannot destroy what it points at, so resolving
|
|
94
|
+
// through it would refuse deletions the denylist never meant to cover.
|
|
95
|
+
const denyCandidate = relative(ctx.root, target).split(sep).join("/");
|
|
96
|
+
if (matchesDenyPaths(denyCandidate, ctx.denyPaths)) {
|
|
97
|
+
return {
|
|
98
|
+
content: `"${shown}" matches this repository's denyPaths configuration, so Delete refuses it regardless of tracked status.`,
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Existence and directory come first so each refusal says the true thing.
|
|
103
|
+
// Tracked-first would report a mistyped path as "not tracked", which is
|
|
104
|
+
// technically true and diagnostically useless.
|
|
105
|
+
let isDirectory;
|
|
106
|
+
try {
|
|
107
|
+
isDirectory = (await lstat(target)).isDirectory();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { content: `"${shown}" does not exist`, isError: true };
|
|
111
|
+
}
|
|
112
|
+
if (isDirectory) {
|
|
113
|
+
// Reachable, not defensive: `ls-files --error-unmatch` exits 0 for a
|
|
114
|
+
// directory holding tracked files, so "tracked" does not imply "file".
|
|
115
|
+
return { content: `"${shown}" is a directory; Delete removes one file at a time`, isError: true };
|
|
116
|
+
}
|
|
117
|
+
const tracked = await gitWithTimeout(["ls-files", "--error-unmatch", "--", target], ctx.root, GIT_TIMEOUT_MS);
|
|
118
|
+
if (tracked.exitCode !== 0) {
|
|
119
|
+
// Untracked. Whether it stays refused now turns on the SECOND git
|
|
120
|
+
// question -- gitignored or not -- rather than stopping here the way
|
|
121
|
+
// the tracked-only rule used to.
|
|
122
|
+
//
|
|
123
|
+
// `check-ignore` has three outcomes and they must NOT be collapsed into
|
|
124
|
+
// two: 0 means ignored, 1 means definitively not ignored, and anything
|
|
125
|
+
// else (128) means git could not answer -- no repository, or a broken
|
|
126
|
+
// one. Only the definitive 1 is an allow. Folding 128 in with 1 would
|
|
127
|
+
// make a repository-less directory the most permissive environment this
|
|
128
|
+
// tool has, which inverts the guard exactly where it is least
|
|
129
|
+
// observable. The pre-nax#1972 code refused "untracked" and "git
|
|
130
|
+
// failed" through one branch on purpose, and that caution survives the
|
|
131
|
+
// three-class rewrite even though its original rationale does not.
|
|
132
|
+
const ignored = await gitWithTimeout(["check-ignore", "-q", "--", target], ctx.root, GIT_TIMEOUT_MS);
|
|
133
|
+
if (ignored.exitCode === 0) {
|
|
134
|
+
return {
|
|
135
|
+
content: `"${shown}" is gitignored, so it exists only on this machine and is not recoverable once deleted. Delete refuses it for that reason.`,
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (ignored.exitCode !== 1) {
|
|
140
|
+
return {
|
|
141
|
+
content: `"${shown}" is not tracked, and git could not determine whether it is ignored, so Delete cannot establish that removing it is safe. Check that this is a git repository.`,
|
|
142
|
+
isError: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await unlink(target);
|
|
148
|
+
return { content: `deleted ${shown} -- stage the removal by passing the same path to GitCommit` };
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name the tool that serves the intent behind a denied call.
|
|
3
|
+
*
|
|
4
|
+
* `policy.ts` already names the granted argv FORMS, which is the right answer
|
|
5
|
+
* when the model wanted a different install. It is the wrong answer when the
|
|
6
|
+
* model wanted `git status`: across three runs, 8 of 32 denied argv calls were
|
|
7
|
+
* for capabilities the session already held as a first-class tool (nax#1937).
|
|
8
|
+
*
|
|
9
|
+
* Lives here rather than in policy.ts because the policy knows the grants but
|
|
10
|
+
* not which tools were ADVERTISED to this session. Naming a tool the session
|
|
11
|
+
* never received would reproduce the defect this exists to fix, so every row
|
|
12
|
+
* is gated on the caller's `available` set.
|
|
13
|
+
*
|
|
14
|
+
* ONE table answers both entry points. There used to be two -- one for argv
|
|
15
|
+
* command lines, one for bare verbs -- covering disjoint command sets, while
|
|
16
|
+
* `redirectForVerb` routed anything multi-token into the argv table. So which
|
|
17
|
+
* table answered depended on whether the model typed a flag, and coverage came
|
|
18
|
+
* out inverted: `ls -la` redirected but `ls` did not, `grep` redirected but
|
|
19
|
+
* `grep -n x y` did not. 20 of 34 denials in the two runs after #1971 carried
|
|
20
|
+
* no redirect (nax#1999). A single table cannot drift against itself.
|
|
21
|
+
*/
|
|
22
|
+
export declare function redirectForArgv(argv: readonly string[], available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Name the tool that serves the intent behind a denied Bash COMMAND.
|
|
25
|
+
*
|
|
26
|
+
* Reads the first segment only: that is what the model reached for, and a
|
|
27
|
+
* later segment's intent is not what to teach when the call is refused. The
|
|
28
|
+
* split is deliberately its own crude one rather than the lexer's
|
|
29
|
+
* (`lexBashCommand`): a command REFUSED for an unanalysable construct must
|
|
30
|
+
* still get a redirect, and the lexer returns nothing to read in that case.
|
|
31
|
+
*/
|
|
32
|
+
export declare function redirectForCommand(command: string, available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Name the tool that serves the intent behind a denied VERB call.
|
|
35
|
+
*
|
|
36
|
+
* `redirectForArgv` is unreachable for RunCommand and Git: they deny through
|
|
37
|
+
* `verbField`, where the policy sees no argv at all, so every such denial was a
|
|
38
|
+
* bare refusal (nax#1971). A verb slot carries either a mini command line the
|
|
39
|
+
* model stuffed there ("ls -la") or a bare word ("grep"); both are tokenized
|
|
40
|
+
* and answered by the same table, which is what keeps them consistent.
|
|
41
|
+
*/
|
|
42
|
+
export declare function redirectForVerb(deniedTool: string, verb: string, available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name the tool that serves the intent behind a denied call.
|
|
3
|
+
*
|
|
4
|
+
* `policy.ts` already names the granted argv FORMS, which is the right answer
|
|
5
|
+
* when the model wanted a different install. It is the wrong answer when the
|
|
6
|
+
* model wanted `git status`: across three runs, 8 of 32 denied argv calls were
|
|
7
|
+
* for capabilities the session already held as a first-class tool (nax#1937).
|
|
8
|
+
*
|
|
9
|
+
* Lives here rather than in policy.ts because the policy knows the grants but
|
|
10
|
+
* not which tools were ADVERTISED to this session. Naming a tool the session
|
|
11
|
+
* never received would reproduce the defect this exists to fix, so every row
|
|
12
|
+
* is gated on the caller's `available` set.
|
|
13
|
+
*
|
|
14
|
+
* ONE table answers both entry points. There used to be two -- one for argv
|
|
15
|
+
* command lines, one for bare verbs -- covering disjoint command sets, while
|
|
16
|
+
* `redirectForVerb` routed anything multi-token into the argv table. So which
|
|
17
|
+
* table answered depended on whether the model typed a flag, and coverage came
|
|
18
|
+
* out inverted: `ls -la` redirected but `ls` did not, `grep` redirected but
|
|
19
|
+
* `grep -n x y` did not. 20 of 34 denials in the two runs after #1971 carried
|
|
20
|
+
* no redirect (nax#1999). A single table cannot drift against itself.
|
|
21
|
+
*/
|
|
22
|
+
const GIT_READ_VERBS = new Set(["diff", "log", "show", "status", "blame"]);
|
|
23
|
+
/**
|
|
24
|
+
* Git verbs that stage or record a commit. Deliberately NOT every write verb:
|
|
25
|
+
* `git restore` reverts, which `GitCommit` does not do, and naming it would be
|
|
26
|
+
* the same defect as naming a tool the session never received.
|
|
27
|
+
*/
|
|
28
|
+
const GIT_COMMIT_VERBS = new Set(["add", "commit"]);
|
|
29
|
+
/**
|
|
30
|
+
* Git verbs that revert tracked paths. Checked separately from the intent
|
|
31
|
+
* table: when the paths are nax's own `.nax/` run state, no tool serves the
|
|
32
|
+
* intent, so the refusal teaches instead of naming one (nax#2007).
|
|
33
|
+
*/
|
|
34
|
+
const GIT_REVERT_VERBS = new Set(["checkout", "restore"]);
|
|
35
|
+
const NAX_OWNED_RUN_STATE_EXPLANATION = "`.nax/` is nax's own run state, written by the harness during this run. It is not part of your diff and must not be reverted.";
|
|
36
|
+
/**
|
|
37
|
+
* The sentence explaining a revert of nax's own run state, or undefined.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately NOT an `Intent`: `render()` wraps every hit as "this session
|
|
40
|
+
* already has `X`", which is false here -- no tool reverts `.nax/`, and the
|
|
41
|
+
* module's header forbids naming one it does not hold. So this predicate is
|
|
42
|
+
* checked BEFORE `intentFor` in both entry points and returns its own sentence.
|
|
43
|
+
*
|
|
44
|
+
* Verb scope is `checkout` and `restore` only; `git stash` is the agent's own
|
|
45
|
+
* WIP, not nax's state, and has no operands to gate on. The gate is a `.nax`
|
|
46
|
+
* path SEGMENT (split on `/`), never a substring, so `src/nax-helpers.ts` and
|
|
47
|
+
* `docs/.naxignore` stay unanswered. `--` is a separator, not an operand, and a
|
|
48
|
+
* `timeout N` wrapper is stripped the same way `intentFor` strips it.
|
|
49
|
+
*/
|
|
50
|
+
function naxOwnedRunStateExplanation(tokens) {
|
|
51
|
+
const av = withoutTimeoutPrefix(tokens);
|
|
52
|
+
const hasGitPrefix = av[0] === "git";
|
|
53
|
+
const verb = hasGitPrefix ? av[1] : av[0];
|
|
54
|
+
if (verb === undefined || !GIT_REVERT_VERBS.has(verb))
|
|
55
|
+
return undefined;
|
|
56
|
+
for (const operand of av.slice(hasGitPrefix ? 2 : 1)) {
|
|
57
|
+
if (operand === "--")
|
|
58
|
+
continue;
|
|
59
|
+
if (operand.split("/").includes(".nax"))
|
|
60
|
+
return NAX_OWNED_RUN_STATE_EXPLANATION;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const GLOB = { tool: "Glob", how: "Glob lists repository paths by pattern" };
|
|
65
|
+
const GREP = { tool: "Grep", how: "Grep searches file contents" };
|
|
66
|
+
const READ = { tool: "Read", how: "Read returns file contents, by line range with offset/limit" };
|
|
67
|
+
const DELETE = { tool: "Delete", how: "Delete removes one tracked file at a time" };
|
|
68
|
+
const GIT = { tool: "Git", how: `Git runs read-only git (${[...GIT_READ_VERBS].join(", ")})` };
|
|
69
|
+
const GIT_COMMIT = {
|
|
70
|
+
tool: "GitCommit",
|
|
71
|
+
how: "GitCommit stages and commits named files -- supply `message` and a `paths` array",
|
|
72
|
+
};
|
|
73
|
+
const TEST_SCOPED = {
|
|
74
|
+
tool: "RunCommand:testScoped",
|
|
75
|
+
how: 'RunCommand {"command":"testScoped"} runs the project test command on named files',
|
|
76
|
+
};
|
|
77
|
+
const BASH = {
|
|
78
|
+
tool: "Bash",
|
|
79
|
+
how: "Bash runs one shell command string, when a Bash(...) allow rule covers every segment of it",
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Commands whose intent the HEAD token alone decides.
|
|
83
|
+
*
|
|
84
|
+
* `rm` and `git` are absent on purpose: their intent is decided by the SECOND
|
|
85
|
+
* token (`rm a.ts` is a Delete, `rm -r dir` is nothing Delete can do; `git log`
|
|
86
|
+
* is Git, `git add` is GitCommit), so they are resolved by shape below.
|
|
87
|
+
*/
|
|
88
|
+
const HEAD_INTENTS = new Map([
|
|
89
|
+
["ls", GLOB],
|
|
90
|
+
["find", GLOB],
|
|
91
|
+
["grep", GREP],
|
|
92
|
+
["cat", READ],
|
|
93
|
+
["bash", BASH],
|
|
94
|
+
["sh", BASH],
|
|
95
|
+
["zsh", BASH],
|
|
96
|
+
// No tool counts lines, and Read with no offset/limit returns a byte-bounded
|
|
97
|
+
// PREFIX -- so counting what it returns under-reports exactly the large files
|
|
98
|
+
// the question gets asked about. Naming Read beats a bare refusal the model
|
|
99
|
+
// retries verbatim, but only if the ceiling is stated rather than implied.
|
|
100
|
+
["wc", { tool: "Read", how: "Read returns file contents, truncated past a size ceiling -- no tool counts lines" }],
|
|
101
|
+
// nax-git-env-allow: a [command, redirect] table entry, not an argv
|
|
102
|
+
["git", GIT],
|
|
103
|
+
...[...GIT_READ_VERBS].map((verb) => [verb, GIT]),
|
|
104
|
+
// `git.ts` declares `allowedVerbs: GIT_READ_VERBS`, so every Git verb-slot
|
|
105
|
+
// denial carries a write verb and arrives BARE -- `commit`, never
|
|
106
|
+
// `git commit`. Reaching these only through the shape branch below would
|
|
107
|
+
// leave the row unreachable from the one slot Git denials come through.
|
|
108
|
+
// Safe as head tokens: `add` is an install verb only in TASK_RUNNERS
|
|
109
|
+
// position (`bun add`), where the head is the runner, not `add`.
|
|
110
|
+
...[...GIT_COMMIT_VERBS].map((verb) => [verb, GIT_COMMIT]),
|
|
111
|
+
]);
|
|
112
|
+
/** Task-runner binaries. A detection heuristic ONLY: what gets named comes
|
|
113
|
+
* entirely from the project's declared commands, never from this list. A runner
|
|
114
|
+
* missing here degrades to the pre-#1971 message rather than to a wrong one.
|
|
115
|
+
*/
|
|
116
|
+
const TASK_RUNNERS = new Set([
|
|
117
|
+
"bun",
|
|
118
|
+
"npm",
|
|
119
|
+
"pnpm",
|
|
120
|
+
"yarn",
|
|
121
|
+
"deno",
|
|
122
|
+
"npx",
|
|
123
|
+
"make",
|
|
124
|
+
"just",
|
|
125
|
+
"task",
|
|
126
|
+
"go",
|
|
127
|
+
"cargo",
|
|
128
|
+
"uv",
|
|
129
|
+
"poetry",
|
|
130
|
+
"pipenv",
|
|
131
|
+
"tox",
|
|
132
|
+
"gradle",
|
|
133
|
+
"mvn",
|
|
134
|
+
]);
|
|
135
|
+
/**
|
|
136
|
+
* Install subcommands. These runners double as package managers, and an install
|
|
137
|
+
* attempt wants the granted install FORMS policy.ts already printed -- not a
|
|
138
|
+
* list of project gates that cannot install anything.
|
|
139
|
+
*/
|
|
140
|
+
const INSTALL_VERBS = new Set(["add", "install", "i", "ci", "get", "sync", "fetch", "mod", "download"]);
|
|
141
|
+
function isSingleFileDelete(tokens, from) {
|
|
142
|
+
return tokens.length === from + 1 && !tokens[from]?.startsWith("-");
|
|
143
|
+
}
|
|
144
|
+
/** A `timeout N ...` prefix wraps the real command; look past it. */
|
|
145
|
+
function withoutTimeoutPrefix(tokens) {
|
|
146
|
+
return tokens[0] === "timeout" ? tokens.slice(2) : tokens;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The one intent table. Shape-dependent heads are resolved first, then the
|
|
150
|
+
* head-only map -- so a bare verb and the same verb with arguments can never
|
|
151
|
+
* be answered differently.
|
|
152
|
+
*/
|
|
153
|
+
function intentFor(tokens) {
|
|
154
|
+
const av = withoutTimeoutPrefix(tokens);
|
|
155
|
+
const [head, second] = av;
|
|
156
|
+
if (head === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (head === "rm")
|
|
159
|
+
return isSingleFileDelete(av, 1) ? DELETE : undefined;
|
|
160
|
+
if (head === "git" && second !== undefined) {
|
|
161
|
+
if (second === "rm")
|
|
162
|
+
return isSingleFileDelete(av, 2) ? DELETE : undefined;
|
|
163
|
+
if (GIT_READ_VERBS.has(second))
|
|
164
|
+
return GIT;
|
|
165
|
+
if (GIT_COMMIT_VERBS.has(second))
|
|
166
|
+
return GIT_COMMIT;
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (head === "bun" && second === "test")
|
|
170
|
+
return TEST_SCOPED;
|
|
171
|
+
return HEAD_INTENTS.get(head);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Name the gates this project declared, for a model that reached for a task
|
|
175
|
+
* runner and wanted to run one of them. What gets named comes from the
|
|
176
|
+
* project's declared commands, never from a hardcoded list (nax#1971).
|
|
177
|
+
*/
|
|
178
|
+
function taskRunnerFallback(tokens, available, declaredCommands) {
|
|
179
|
+
const av = withoutTimeoutPrefix(tokens);
|
|
180
|
+
const head = av[0];
|
|
181
|
+
if (head === undefined || !TASK_RUNNERS.has(head))
|
|
182
|
+
return undefined;
|
|
183
|
+
const sub = av[1];
|
|
184
|
+
if (sub !== undefined && INSTALL_VERBS.has(sub))
|
|
185
|
+
return undefined;
|
|
186
|
+
if (!available.has("RunCommand") || declaredCommands.size === 0)
|
|
187
|
+
return undefined;
|
|
188
|
+
return `this session already has RunCommand with declared commands: ${[...declaredCommands].join(", ")}`;
|
|
189
|
+
}
|
|
190
|
+
function render(hit, available, declaredCommands) {
|
|
191
|
+
if (hit.tool === TEST_SCOPED.tool) {
|
|
192
|
+
// Conditioned on the project actually declaring the command, not hardcoded:
|
|
193
|
+
// naming a command this project never declared is the same defect again.
|
|
194
|
+
if (!available.has("RunCommand") || !declaredCommands.has("testScoped"))
|
|
195
|
+
return undefined;
|
|
196
|
+
return `this session already has ${hit.how}`;
|
|
197
|
+
}
|
|
198
|
+
if (!available.has(hit.tool))
|
|
199
|
+
return undefined;
|
|
200
|
+
return `this session already has \`${hit.tool}\` -- ${hit.how}`;
|
|
201
|
+
}
|
|
202
|
+
export function redirectForArgv(argv, available, declaredCommands) {
|
|
203
|
+
const ownedRunState = naxOwnedRunStateExplanation(argv);
|
|
204
|
+
if (ownedRunState !== undefined)
|
|
205
|
+
return ownedRunState;
|
|
206
|
+
const hit = intentFor(argv);
|
|
207
|
+
if (hit === undefined)
|
|
208
|
+
return taskRunnerFallback(argv, available, declaredCommands);
|
|
209
|
+
return render(hit, available, declaredCommands);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Name the tool that serves the intent behind a denied Bash COMMAND.
|
|
213
|
+
*
|
|
214
|
+
* Reads the first segment only: that is what the model reached for, and a
|
|
215
|
+
* later segment's intent is not what to teach when the call is refused. The
|
|
216
|
+
* split is deliberately its own crude one rather than the lexer's
|
|
217
|
+
* (`lexBashCommand`): a command REFUSED for an unanalysable construct must
|
|
218
|
+
* still get a redirect, and the lexer returns nothing to read in that case.
|
|
219
|
+
*/
|
|
220
|
+
export function redirectForCommand(command, available, declaredCommands) {
|
|
221
|
+
const [first = ""] = command.split(/&&|\|\||;|\||\n/);
|
|
222
|
+
const tokens = first
|
|
223
|
+
.trim()
|
|
224
|
+
.split(/\s+/)
|
|
225
|
+
.filter((token) => token.length > 0);
|
|
226
|
+
if (tokens.length === 0)
|
|
227
|
+
return undefined;
|
|
228
|
+
const ownedRunState = naxOwnedRunStateExplanation(tokens);
|
|
229
|
+
if (ownedRunState !== undefined)
|
|
230
|
+
return ownedRunState;
|
|
231
|
+
const hit = intentFor(tokens);
|
|
232
|
+
if (hit === undefined)
|
|
233
|
+
return taskRunnerFallback(tokens, available, declaredCommands);
|
|
234
|
+
// Telling Bash it already has Bash reads as a contradiction of the denial —
|
|
235
|
+
// the same suppression redirectForVerb applies.
|
|
236
|
+
if (hit.tool === "Bash")
|
|
237
|
+
return undefined;
|
|
238
|
+
return render(hit, available, declaredCommands);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Name the tool that serves the intent behind a denied VERB call.
|
|
242
|
+
*
|
|
243
|
+
* `redirectForArgv` is unreachable for RunCommand and Git: they deny through
|
|
244
|
+
* `verbField`, where the policy sees no argv at all, so every such denial was a
|
|
245
|
+
* bare refusal (nax#1971). A verb slot carries either a mini command line the
|
|
246
|
+
* model stuffed there ("ls -la") or a bare word ("grep"); both are tokenized
|
|
247
|
+
* and answered by the same table, which is what keeps them consistent.
|
|
248
|
+
*/
|
|
249
|
+
export function redirectForVerb(deniedTool, verb, available, declaredCommands) {
|
|
250
|
+
const tokens = verb
|
|
251
|
+
.trim()
|
|
252
|
+
.split(/\s+/)
|
|
253
|
+
.filter((token) => token.length > 0);
|
|
254
|
+
if (tokens.length === 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
const ownedRunState = naxOwnedRunStateExplanation(tokens);
|
|
257
|
+
if (ownedRunState !== undefined)
|
|
258
|
+
return ownedRunState;
|
|
259
|
+
const hit = intentFor(tokens);
|
|
260
|
+
if (hit === undefined)
|
|
261
|
+
return taskRunnerFallback(tokens, available, declaredCommands);
|
|
262
|
+
// Telling Git it already has Git reads as a contradiction of the denial.
|
|
263
|
+
if (hit.tool === deniedTool)
|
|
264
|
+
return undefined;
|
|
265
|
+
return render(hit, available, declaredCommands);
|
|
266
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match a Delete candidate against the repo-configurable `denyPaths`
|
|
3
|
+
* denylist (nax#1972, `execution.denyPaths` in config).
|
|
4
|
+
*
|
|
5
|
+
* A separate file rather than an addition to src/tools/policy.ts on purpose:
|
|
6
|
+
* that file already carries the containment seam and is close to the
|
|
7
|
+
* project's file-size ratchet, and denyPaths is a narrower, single-tool
|
|
8
|
+
* concern -- it has nothing to do with resolving or containing a path, only
|
|
9
|
+
* with refusing one the policy already approved.
|
|
10
|
+
*
|
|
11
|
+
* The glob syntax deliberately matches globToRegExp in policy.ts (`**` spans
|
|
12
|
+
* separators, `*` does not) so a `denyPaths` entry reads the same way a
|
|
13
|
+
* ToolGrant pattern does elsewhere in config -- one mental model for "what
|
|
14
|
+
* does a glob mean in nax config", not two subtly different ones. The
|
|
15
|
+
* implementation is a small, independent copy rather than an export from
|
|
16
|
+
* policy.ts: that keeps this file free to be read (and reasoned about) on
|
|
17
|
+
* its own, and keeps policy.ts's containment logic untouched by a change
|
|
18
|
+
* that has nothing to do with containment.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Does `relativePath` match any pattern in `denyPaths`?
|
|
22
|
+
*
|
|
23
|
+
* `relativePath` MUST already be canonical and repo-relative -- the caller
|
|
24
|
+
* derives it from the resolved entry, never from `input.path`. This is not a
|
|
25
|
+
* stylistic preference: matching the caller's own spelling means the denylist
|
|
26
|
+
* and the policy that approved the call are reading different strings, and
|
|
27
|
+
* every alternate spelling of one file ("./x", "x//y", "a/../x") walks past
|
|
28
|
+
* the denylist (nax#1972, reproduced). Repo-relative rather than absolute so
|
|
29
|
+
* a pattern is rooted at the repo, not at the filesystem.
|
|
30
|
+
*
|
|
31
|
+
* Both sides are NFC-normalized before comparison. macOS stores filenames
|
|
32
|
+
* decomposed (NFD), so a pattern typed in a config file and the path that
|
|
33
|
+
* comes back off disk can be byte-different while naming one file.
|
|
34
|
+
*/
|
|
35
|
+
export declare function matchesDenyPaths(relativePath: string, denyPaths: readonly string[] | undefined): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match a Delete candidate against the repo-configurable `denyPaths`
|
|
3
|
+
* denylist (nax#1972, `execution.denyPaths` in config).
|
|
4
|
+
*
|
|
5
|
+
* A separate file rather than an addition to src/tools/policy.ts on purpose:
|
|
6
|
+
* that file already carries the containment seam and is close to the
|
|
7
|
+
* project's file-size ratchet, and denyPaths is a narrower, single-tool
|
|
8
|
+
* concern -- it has nothing to do with resolving or containing a path, only
|
|
9
|
+
* with refusing one the policy already approved.
|
|
10
|
+
*
|
|
11
|
+
* The glob syntax deliberately matches globToRegExp in policy.ts (`**` spans
|
|
12
|
+
* separators, `*` does not) so a `denyPaths` entry reads the same way a
|
|
13
|
+
* ToolGrant pattern does elsewhere in config -- one mental model for "what
|
|
14
|
+
* does a glob mean in nax config", not two subtly different ones. The
|
|
15
|
+
* implementation is a small, independent copy rather than an export from
|
|
16
|
+
* policy.ts: that keeps this file free to be read (and reasoned about) on
|
|
17
|
+
* its own, and keeps policy.ts's containment logic untouched by a change
|
|
18
|
+
* that has nothing to do with containment.
|
|
19
|
+
*/
|
|
20
|
+
function globToRegExp(pattern) {
|
|
21
|
+
let out = "";
|
|
22
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
23
|
+
const char = pattern[i];
|
|
24
|
+
if (char === "*") {
|
|
25
|
+
if (pattern[i + 1] === "*") {
|
|
26
|
+
i += 1;
|
|
27
|
+
if (pattern[i + 1] === "/") {
|
|
28
|
+
out += "(?:.*/)?";
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
out += ".*";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
out += "[^/]*";
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (char === "?") {
|
|
41
|
+
out += "[^/]";
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
}
|
|
46
|
+
// Case-insensitive, and both sides NFC-normalized at the call site.
|
|
47
|
+
// macOS and Windows default to case-insensitive filesystems, where ".ENV"
|
|
48
|
+
// and ".env" are one file -- a case-sensitive compare there lets a denied
|
|
49
|
+
// path be deleted by respelling it (nax#1972, reproduced). Applying the
|
|
50
|
+
// flag unconditionally rather than probing the filesystem keeps the rule
|
|
51
|
+
// one predictable thing: on a case-sensitive filesystem this over-refuses
|
|
52
|
+
// a genuinely distinct ".ENV", which is the direction a denylist should
|
|
53
|
+
// fail in.
|
|
54
|
+
return new RegExp(`^${out}$`, "i");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Does `relativePath` match any pattern in `denyPaths`?
|
|
58
|
+
*
|
|
59
|
+
* `relativePath` MUST already be canonical and repo-relative -- the caller
|
|
60
|
+
* derives it from the resolved entry, never from `input.path`. This is not a
|
|
61
|
+
* stylistic preference: matching the caller's own spelling means the denylist
|
|
62
|
+
* and the policy that approved the call are reading different strings, and
|
|
63
|
+
* every alternate spelling of one file ("./x", "x//y", "a/../x") walks past
|
|
64
|
+
* the denylist (nax#1972, reproduced). Repo-relative rather than absolute so
|
|
65
|
+
* a pattern is rooted at the repo, not at the filesystem.
|
|
66
|
+
*
|
|
67
|
+
* Both sides are NFC-normalized before comparison. macOS stores filenames
|
|
68
|
+
* decomposed (NFD), so a pattern typed in a config file and the path that
|
|
69
|
+
* comes back off disk can be byte-different while naming one file.
|
|
70
|
+
*/
|
|
71
|
+
export function matchesDenyPaths(relativePath, denyPaths) {
|
|
72
|
+
if (denyPaths === undefined || denyPaths.length === 0)
|
|
73
|
+
return false;
|
|
74
|
+
const candidate = relativePath.normalize("NFC");
|
|
75
|
+
return denyPaths.some((pattern) => globToRegExp(pattern.normalize("NFC")).test(candidate));
|
|
76
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
/**
|
|
23
|
+
* Inputs to `composeEditRegion`.
|
|
24
|
+
*
|
|
25
|
+
* `matchIndex` is the offset `source.indexOf(oldString)` found — the caller
|
|
26
|
+
* wrote the new text there, so `updated.slice(0, matchIndex)` is identical to
|
|
27
|
+
* the source prefix and the start line can be counted on either.
|
|
28
|
+
*/
|
|
29
|
+
export interface EditRegionInput {
|
|
30
|
+
readonly updated: string;
|
|
31
|
+
readonly matchIndex: number;
|
|
32
|
+
readonly newStringLength: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The bounded, Read-compatible view of the region an Edit wrote: a
|
|
36
|
+
* `[lines a-b of N]` header and the selected lines, or `[file is now empty]`
|
|
37
|
+
* for an emptied file. Has no trailing newline, and cannot throw on any
|
|
38
|
+
* string input.
|
|
39
|
+
*/
|
|
40
|
+
export declare function composeEditRegion({ updated, matchIndex, newStringLength }: EditRegionInput): string;
|
|
41
|
+
/**
|
|
42
|
+
* Replace the single occurrence of `oldString` at `matchIndex` with
|
|
43
|
+
* `newString`, literally.
|
|
44
|
+
*
|
|
45
|
+
* The caller has already established uniqueness (`countOccurrences` in
|
|
46
|
+
* `edit.ts`) and passes the index `source.indexOf(oldString)` found, so this
|
|
47
|
+
* is pure slicing: everything before the match, the new text verbatim, and
|
|
48
|
+
* everything after it. No `replace`, and therefore no substitution patterns.
|
|
49
|
+
*/
|
|
50
|
+
export declare function replaceUniqueLiteral(source: string, oldString: string, newString: string, matchIndex: number): string;
|