@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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairs the compiled policy with the tool registry and answers one call.
|
|
3
|
+
*
|
|
4
|
+
* nax-permission-mode-allow: consumes grants resolved by resolvePermissions;
|
|
5
|
+
* makes no permission decision of its own.
|
|
6
|
+
*
|
|
7
|
+
* The three outcomes are kept structurally distinct. Reusing one channel for a
|
|
8
|
+
* refusal and a crash would make a denied permission look like a recoverable
|
|
9
|
+
* tool error, which ADR-029 section 5 forbids.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
13
|
+
import { headlessAskResolver } from "#src/permissions/index";
|
|
14
|
+
import { deleteTool } from "./delete.js";
|
|
15
|
+
import { editTool } from "./edit.js";
|
|
16
|
+
import { gitTool } from "./git.js";
|
|
17
|
+
import { gitCommitTool } from "./git-commit.js";
|
|
18
|
+
import { globTool } from "./glob.js";
|
|
19
|
+
import { grepTool } from "./grep.js";
|
|
20
|
+
import { readTool } from "./read.js";
|
|
21
|
+
import { getCodingTool, registerBuiltinTool } from "./registry.js";
|
|
22
|
+
import { requestCapabilityTool } from "./request-capability.js";
|
|
23
|
+
import { openCallShadowTap, resolveAskOutcome, resolveDenialOutcome, resolveGuardDenial, resolvePolicyIdentity, } from "./runtime-calltool.js";
|
|
24
|
+
import { scratchpadListTool, scratchpadReadTool, scratchpadWriteTool } from "./scratchpad.js";
|
|
25
|
+
import { applyModelTruncationPolicy } from "./spill.js";
|
|
26
|
+
import { createNoOpToolAuditSink } from "./tool-audit.js";
|
|
27
|
+
import { READ_CEILING } from "./truncate.js";
|
|
28
|
+
import { writeTool } from "./write.js";
|
|
29
|
+
/** Per-call output ceiling, mirroring ToolDescriptor.maxTokensPerCall in spirit. */
|
|
30
|
+
export const DEFAULT_TOOL_MAX_BYTES = 40_000;
|
|
31
|
+
/**
|
|
32
|
+
* Largest file a tool will read whole or write at all.
|
|
33
|
+
*
|
|
34
|
+
* Distinct from DEFAULT_TOOL_MAX_BYTES because they answer different questions:
|
|
35
|
+
* that one is how much the model may be told, this one is how much file a tool
|
|
36
|
+
* will handle. Edit must read and rewrite a file entirely, so it cannot use the
|
|
37
|
+
* output ceiling without refusing ordinary source files.
|
|
38
|
+
*
|
|
39
|
+
* Generous on purpose. Nothing an LLM legitimately edits approaches it, and a
|
|
40
|
+
* limit that fires during normal work would be worked around rather than
|
|
41
|
+
* respected.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_TOOL_MAX_FILE_BYTES = 2_000_000;
|
|
44
|
+
/** Injectable logger seam, mirroring _pullToolsDeps.getLogger. */
|
|
45
|
+
export const _codingToolDeps = { getLogger: getSafeLogger };
|
|
46
|
+
let builtinsRegistered = false;
|
|
47
|
+
/** Idempotent: the registry is process-global, the runtime is per-session. */
|
|
48
|
+
export function registerBuiltinCodingTools() {
|
|
49
|
+
if (builtinsRegistered)
|
|
50
|
+
return;
|
|
51
|
+
for (const tool of [
|
|
52
|
+
readTool,
|
|
53
|
+
globTool,
|
|
54
|
+
grepTool,
|
|
55
|
+
writeTool,
|
|
56
|
+
editTool,
|
|
57
|
+
deleteTool,
|
|
58
|
+
gitTool,
|
|
59
|
+
gitCommitTool,
|
|
60
|
+
requestCapabilityTool,
|
|
61
|
+
scratchpadWriteTool,
|
|
62
|
+
scratchpadReadTool,
|
|
63
|
+
scratchpadListTool,
|
|
64
|
+
]) {
|
|
65
|
+
if (getCodingTool(tool.name) === undefined)
|
|
66
|
+
registerBuiltinTool(tool);
|
|
67
|
+
}
|
|
68
|
+
builtinsRegistered = true;
|
|
69
|
+
}
|
|
70
|
+
/** @internal Test-only: pairs with _resetRegistryForTest. */
|
|
71
|
+
export function _resetBuiltinsForTest() {
|
|
72
|
+
builtinsRegistered = false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The run-scoped ports every ToolRunContext carries, present-field only.
|
|
76
|
+
* Built once per runtime and kept out of `runTool`, which sits at its
|
|
77
|
+
* complexity baseline.
|
|
78
|
+
*/
|
|
79
|
+
function contextPorts(opts) {
|
|
80
|
+
return {
|
|
81
|
+
...(opts.interceptor !== undefined ? { interceptor: opts.interceptor } : {}),
|
|
82
|
+
...(opts.protectedPaths !== undefined ? { protectedPaths: opts.protectedPaths } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function createCodingToolRuntime(opts) {
|
|
86
|
+
registerBuiltinCodingTools();
|
|
87
|
+
// The global registry cannot hold session-local tools like RunCommand (its
|
|
88
|
+
// declared commands are per-project). Consult this layer before the registry.
|
|
89
|
+
const extra = new Map((opts.extraTools ?? []).map((t) => [t.name, t]));
|
|
90
|
+
const lookup = (name) => extra.get(name) ?? getCodingTool(name);
|
|
91
|
+
const sink = opts.sink ?? createNoOpToolAuditSink();
|
|
92
|
+
const ports = contextPorts(opts);
|
|
93
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_TOOL_MAX_BYTES;
|
|
94
|
+
const maxFileBytes = opts.maxFileBytes ?? DEFAULT_TOOL_MAX_FILE_BYTES;
|
|
95
|
+
const readCeiling = opts.readCeiling ?? READ_CEILING;
|
|
96
|
+
const askResolver = opts.askResolver ?? headlessAskResolver();
|
|
97
|
+
const granted = new Set(opts.policy.grantedTools());
|
|
98
|
+
// Captured so the `ToolRunContext` constructed inside `runTool` carries the
|
|
99
|
+
// session-wide abort signal; tools (Bash, Exec's argv branch) forward it
|
|
100
|
+
// into their launcher / runArgv call so turn cancellation reaches the
|
|
101
|
+
// process group. US-001.
|
|
102
|
+
const signal = opts.signal;
|
|
103
|
+
// What `advertised()` actually returned, so a denial can name only tools the
|
|
104
|
+
// session really received. Recomputing from `granted` would be wrong: an op
|
|
105
|
+
// narrows the set by declaring fewer tools than it was granted.
|
|
106
|
+
let advertisedNames = new Set();
|
|
107
|
+
/**
|
|
108
|
+
* One line per call, mirroring the pull-tool subsystem's `invoked` record.
|
|
109
|
+
*
|
|
110
|
+
* Every outcome is logged, denials included: a refused call that leaves no
|
|
111
|
+
* trace is indistinguishable from a call never made, and telling those two
|
|
112
|
+
* apart is the whole reason this exists.
|
|
113
|
+
*
|
|
114
|
+
* The logger keeps its calls for operator visibility. The audit sink is the
|
|
115
|
+
* durable copy a later decision reads from; the two are not interchangeable.
|
|
116
|
+
*/
|
|
117
|
+
function log(tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation) {
|
|
118
|
+
// The level is the console filter: `normal` mode drops debug, and the file
|
|
119
|
+
// sink writes every level regardless, so demoting keeps the record without
|
|
120
|
+
// spending an operator's attention on it. A breach is the one outcome that
|
|
121
|
+
// can indicate prompt injection, so it alone reaches `error`.
|
|
122
|
+
const level = outcome === "denied" || outcome === "denied:ask"
|
|
123
|
+
? breach === true
|
|
124
|
+
? "error"
|
|
125
|
+
: "warn"
|
|
126
|
+
: outcome === "error" && !routineErrors
|
|
127
|
+
? "warn"
|
|
128
|
+
: "debug";
|
|
129
|
+
// Never parse the namespaced name apart — the provider id is carried
|
|
130
|
+
// explicitly precisely so a naming-convention change cannot break
|
|
131
|
+
// telemetry silently. `tool` is the identity the ledger records: the
|
|
132
|
+
// namespaced name for a provider tool, `Exec` for the argv branch (which
|
|
133
|
+
// is not provider-supplied, so the lookup misses and the field is absent).
|
|
134
|
+
const provider = opts.providerIdByTool?.get(tool);
|
|
135
|
+
// `reason` rides under the `error` key because the formatter's
|
|
136
|
+
// readFailureReason() renders exactly that key on warn/error lines. The
|
|
137
|
+
// message names the tool so the line is legible without the JSONL: these
|
|
138
|
+
// used to print as a bare "coding-tool invoked" with neither.
|
|
139
|
+
_codingToolDeps.getLogger()?.[level]("coding-tool", `${tool} ${outcome}`, {
|
|
140
|
+
storyId: opts.storyId,
|
|
141
|
+
tool,
|
|
142
|
+
outcome,
|
|
143
|
+
resultBytes,
|
|
144
|
+
...(reason !== undefined && reason.length > 0 ? { error: reason } : {}),
|
|
145
|
+
});
|
|
146
|
+
sink.record({
|
|
147
|
+
tool,
|
|
148
|
+
outcome,
|
|
149
|
+
breach,
|
|
150
|
+
input,
|
|
151
|
+
resultBytes,
|
|
152
|
+
storyId: opts.storyId,
|
|
153
|
+
at: new Date().toISOString(),
|
|
154
|
+
...(reason !== undefined && reason.length > 0 ? { reason } : {}),
|
|
155
|
+
...(audit?.executed !== undefined ? { executed: audit.executed } : {}),
|
|
156
|
+
...(audit?.target !== undefined ? { target: audit.target } : {}),
|
|
157
|
+
...(audit?.approval !== undefined ? { approval: audit.approval } : {}),
|
|
158
|
+
...(audit?.sandbox !== undefined ? { sandbox: audit.sandbox } : {}),
|
|
159
|
+
...(audit?.exitCode !== undefined ? { exitCode: audit.exitCode } : {}),
|
|
160
|
+
...(provider !== undefined ? { provider } : {}),
|
|
161
|
+
...(resultBytesPreTruncation !== undefined ? { resultBytesPreTruncation } : {}),
|
|
162
|
+
...(opts.callId !== undefined ? { callId: opts.callId } : {}),
|
|
163
|
+
...(opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}),
|
|
164
|
+
...(context?.turnId !== undefined ? { turnId: context.turnId } : {}),
|
|
165
|
+
...(context?.roundTrips !== undefined ? { roundTrips: context.roundTrips } : {}),
|
|
166
|
+
...(context?.toolCallId !== undefined ? { toolCallId: context.toolCallId } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function shapeToolResult(body, toolName, context) {
|
|
170
|
+
if (context?.deferModelTruncation === true)
|
|
171
|
+
return body;
|
|
172
|
+
return applyModelTruncationPolicy(body, {
|
|
173
|
+
toolName,
|
|
174
|
+
callId: context?.toolCallId ?? randomUUID(),
|
|
175
|
+
root: opts.policy.root,
|
|
176
|
+
maxBytes,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
advertised(declared) {
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const name of declared) {
|
|
183
|
+
if (!granted.has(name))
|
|
184
|
+
continue;
|
|
185
|
+
const tool = lookup(name);
|
|
186
|
+
if (tool !== undefined)
|
|
187
|
+
out.push(tool);
|
|
188
|
+
}
|
|
189
|
+
advertisedNames = new Set(out.map((t) => t.name));
|
|
190
|
+
return out;
|
|
191
|
+
},
|
|
192
|
+
async callTool(name, input, context) {
|
|
193
|
+
const tool = lookup(name);
|
|
194
|
+
if (tool === undefined) {
|
|
195
|
+
const reason = `unknown tool "${name}"`;
|
|
196
|
+
log(name, "denied", 0, input, context, false, reason);
|
|
197
|
+
return { kind: "denied", reason, breach: false };
|
|
198
|
+
}
|
|
199
|
+
const { policyIdentity, argvField, hasArgv } = resolvePolicyIdentity(name, tool, input);
|
|
200
|
+
const verdict = opts.policy.check(policyIdentity, tool.scope, input);
|
|
201
|
+
// P5: observe the command now (not awaited), settle from logCall below.
|
|
202
|
+
const tap = openCallShadowTap({
|
|
203
|
+
shadow: opts.commandShadow,
|
|
204
|
+
identity: policyIdentity,
|
|
205
|
+
tool,
|
|
206
|
+
input,
|
|
207
|
+
hasArgv,
|
|
208
|
+
argvField,
|
|
209
|
+
root: opts.policy.root,
|
|
210
|
+
stage: opts.pipelineStage,
|
|
211
|
+
storyId: opts.storyId,
|
|
212
|
+
callId: opts.callId,
|
|
213
|
+
scopeId: opts.scopeId,
|
|
214
|
+
context,
|
|
215
|
+
verdict,
|
|
216
|
+
});
|
|
217
|
+
// Every ledger outcome of this call settles the tap exactly once, with
|
|
218
|
+
// `denied:ask` and `decidedBy` intact -- which CodingToolOutcome.kind
|
|
219
|
+
// alone would lose (spec 4.2). The audit is a named parameter, not a
|
|
220
|
+
// rest-array index: `log`'s positional shape can then change without
|
|
221
|
+
// silently shifting what the tap settles with.
|
|
222
|
+
const logCall = (tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation) => {
|
|
223
|
+
log(tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation);
|
|
224
|
+
tap?.settle(outcome, audit?.approval?.decidedBy, audit?.executed === undefined
|
|
225
|
+
? undefined
|
|
226
|
+
: { executed: audit.executed, ...(audit.cwd !== undefined ? { cwd: audit.cwd } : {}) });
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Executes a permitted call and records its outcome. Shared by the
|
|
230
|
+
* ordinary allow path and an ask verdict an AskResolver approved, so an
|
|
231
|
+
* approved call behaves exactly as a grant would have.
|
|
232
|
+
*
|
|
233
|
+
* The result is shaped by `applyModelTruncationPolicy` before it is
|
|
234
|
+
* returned, which is what makes the model-facing cap one policy rather
|
|
235
|
+
* than one per tool: the tools bound their own I/O at `ctx.readCeiling`,
|
|
236
|
+
* and this is where the byte/line ceilings the model experiences — and
|
|
237
|
+
* the spill of whatever they cut — are applied. The native session
|
|
238
|
+
* applies the same policy at its `after_tool` chokepoint, so a call made
|
|
239
|
+
* through the loop and one made directly see the same shaping.
|
|
240
|
+
*/
|
|
241
|
+
async function runTool(target, callInput, resolvedPaths, approval) {
|
|
242
|
+
// US-002 AC15: a per-call `ToolCallContext.signal` (the native batch's
|
|
243
|
+
// turn signal) takes priority over the runtime-level signal. Both
|
|
244
|
+
// describe "abort this in-flight tool"; the per-call one is the one
|
|
245
|
+
// the batch explicitly put in scope, so it wins when both are
|
|
246
|
+
// present, falling back to the runtime-level signal otherwise so the
|
|
247
|
+
// session-wide abort keeps working when no per-call signal is in
|
|
248
|
+
// scope.
|
|
249
|
+
const callSignal = context?.signal ?? signal;
|
|
250
|
+
try {
|
|
251
|
+
const result = await target.run(callInput, {
|
|
252
|
+
root: opts.policy.root,
|
|
253
|
+
resolvedPaths,
|
|
254
|
+
maxBytes,
|
|
255
|
+
maxFileBytes,
|
|
256
|
+
readCeiling,
|
|
257
|
+
...(opts.denyPaths !== undefined ? { denyPaths: opts.denyPaths } : {}),
|
|
258
|
+
...ports,
|
|
259
|
+
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
260
|
+
});
|
|
261
|
+
const kind = result.isError === true ? "error" : "ok";
|
|
262
|
+
const content = await shapeToolResult(result.content, policyIdentity, context);
|
|
263
|
+
const record = (finalContent) => logCall(policyIdentity, kind, finalContent.length, callInput, context, false, kind === "error" ? result.content : undefined, target.routineErrors, { ...result.audit, ...(approval ? { approval } : {}) }, result.resultBytesPreTruncation);
|
|
264
|
+
if (context?.deferModelTruncation === true)
|
|
265
|
+
return { kind, content, finalizeAudit: record };
|
|
266
|
+
record(content);
|
|
267
|
+
return { kind, content };
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
const rawContent = err instanceof Error ? err.message : String(err);
|
|
271
|
+
const content = await shapeToolResult(rawContent, policyIdentity, context);
|
|
272
|
+
const record = (finalContent) => logCall(policyIdentity, "error", finalContent.length, callInput, context, false, rawContent, target.routineErrors, approval ? { approval } : undefined);
|
|
273
|
+
if (context?.deferModelTruncation === true) {
|
|
274
|
+
return { kind: "error", content, finalizeAudit: record };
|
|
275
|
+
}
|
|
276
|
+
record(content);
|
|
277
|
+
return { kind: "error", content };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!verdict.allowed && verdict.outcome === "ask") {
|
|
281
|
+
return resolveAskOutcome({
|
|
282
|
+
identity: policyIdentity,
|
|
283
|
+
tool,
|
|
284
|
+
input,
|
|
285
|
+
context,
|
|
286
|
+
verdict,
|
|
287
|
+
runtimeSignal: signal,
|
|
288
|
+
askResolver,
|
|
289
|
+
stage: opts.pipelineStage,
|
|
290
|
+
storyId: opts.storyId,
|
|
291
|
+
root: opts.policy.root,
|
|
292
|
+
logCall,
|
|
293
|
+
runTool,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (!verdict.allowed) {
|
|
297
|
+
return resolveDenialOutcome({
|
|
298
|
+
name,
|
|
299
|
+
identity: policyIdentity,
|
|
300
|
+
tool,
|
|
301
|
+
input,
|
|
302
|
+
context,
|
|
303
|
+
verdict,
|
|
304
|
+
argvField,
|
|
305
|
+
advertisedNames,
|
|
306
|
+
declaredCommands: opts.declaredCommands,
|
|
307
|
+
root: opts.policy.root,
|
|
308
|
+
logCall,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// US-004: an ALLOWED `Bash`/`Exec` call the configured guard flags is
|
|
312
|
+
// routed through the existing ask channel; every other call (no guard,
|
|
313
|
+
// unflagged, any other identity) runs exactly as before. The tap above
|
|
314
|
+
// was opened BEFORE this await, so the shadow's observation starts the
|
|
315
|
+
// shared classification the guard then reuses.
|
|
316
|
+
const guardDenial = await resolveGuardDenial({
|
|
317
|
+
identity: policyIdentity,
|
|
318
|
+
tool,
|
|
319
|
+
input,
|
|
320
|
+
argvField,
|
|
321
|
+
guard: opts.commandShadow?.guard,
|
|
322
|
+
root: opts.policy.root,
|
|
323
|
+
tempConfined: opts.tempConfined ?? false,
|
|
324
|
+
resolvedPaths: verdict.resolvedPaths,
|
|
325
|
+
});
|
|
326
|
+
if (guardDenial !== undefined) {
|
|
327
|
+
return resolveAskOutcome({
|
|
328
|
+
identity: policyIdentity,
|
|
329
|
+
tool,
|
|
330
|
+
input,
|
|
331
|
+
context,
|
|
332
|
+
verdict: guardDenial,
|
|
333
|
+
runtimeSignal: signal,
|
|
334
|
+
askResolver,
|
|
335
|
+
stage: opts.pipelineStage,
|
|
336
|
+
storyId: opts.storyId,
|
|
337
|
+
root: opts.policy.root,
|
|
338
|
+
logCall,
|
|
339
|
+
runTool,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return runTool(tool, input, verdict.resolvedPaths);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confined write/read/list tools for an agent-owned throwaway directory.
|
|
3
|
+
*
|
|
4
|
+
* Three small tools (ScratchpadWrite / ScratchpadRead / ScratchpadList) that
|
|
5
|
+
* let a session keep notes, command output and intermediate lists between
|
|
6
|
+
* calls without ever writing anything the policy could later attribute to
|
|
7
|
+
* the run. Containment is the load-bearing seam: every tool declares
|
|
8
|
+
* `scope.confineTo = SCRATCHPAD_DIR`, so the policy runs `resolveWithin`
|
|
9
|
+
* against `<root>/<confineTo>` instead of the repository root. A `..`
|
|
10
|
+
* traversal leaves the confined directory, returns null from `resolveWithin`,
|
|
11
|
+
* and the policy reports `breach: true` (US-001). The tools themselves never
|
|
12
|
+
* resolve a path -- they consume `ctx.resolvedPaths[0]` only, so the policy
|
|
13
|
+
* stays the single seam.
|
|
14
|
+
*/
|
|
15
|
+
import type { CodingTool } from "./registry.ts";
|
|
16
|
+
/** The canonical scratchpad path, written into the policy as `scope.confineTo`. */
|
|
17
|
+
export declare const SCRATCHPAD_DIR = ".nax/scratchpad";
|
|
18
|
+
export declare const scratchpadWriteTool: CodingTool;
|
|
19
|
+
export declare const scratchpadReadTool: CodingTool;
|
|
20
|
+
export declare const scratchpadListTool: CodingTool;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confined write/read/list tools for an agent-owned throwaway directory.
|
|
3
|
+
*
|
|
4
|
+
* Three small tools (ScratchpadWrite / ScratchpadRead / ScratchpadList) that
|
|
5
|
+
* let a session keep notes, command output and intermediate lists between
|
|
6
|
+
* calls without ever writing anything the policy could later attribute to
|
|
7
|
+
* the run. Containment is the load-bearing seam: every tool declares
|
|
8
|
+
* `scope.confineTo = SCRATCHPAD_DIR`, so the policy runs `resolveWithin`
|
|
9
|
+
* against `<root>/<confineTo>` instead of the repository root. A `..`
|
|
10
|
+
* traversal leaves the confined directory, returns null from `resolveWithin`,
|
|
11
|
+
* and the policy reports `breach: true` (US-001). The tools themselves never
|
|
12
|
+
* resolve a path -- they consume `ctx.resolvedPaths[0]` only, so the policy
|
|
13
|
+
* stays the single seam.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join, sep } from "node:path";
|
|
18
|
+
import { fileSizeOrZero } from "#src/internal/file-size";
|
|
19
|
+
import { getAgentRuntime } from "#src/runtime/index";
|
|
20
|
+
import { resolveWithin } from "./policy.js";
|
|
21
|
+
import { readFileSlice } from "./read-file.js";
|
|
22
|
+
import { READ_CEILING } from "./truncate.js";
|
|
23
|
+
/** The canonical scratchpad path, written into the policy as `scope.confineTo`. */
|
|
24
|
+
export const SCRATCHPAD_DIR = ".nax/scratchpad";
|
|
25
|
+
/**
|
|
26
|
+
* Match cap for ScratchpadList. Mirrors `glob.ts`'s `MAX_MATCHES` -- a
|
|
27
|
+
* long-running session accumulates throwaway files by design, so an
|
|
28
|
+
* unbounded scan would feed an arbitrarily large listing back into model
|
|
29
|
+
* context, bypassing the model-facing ceiling the after_tool policy
|
|
30
|
+
* applies to every other content-returning tool.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_MATCHES = 500;
|
|
33
|
+
export const scratchpadWriteTool = {
|
|
34
|
+
name: "ScratchpadWrite",
|
|
35
|
+
description: "Write a throwaway file to your scratchpad at .nax/scratchpad/. Use it for notes to yourself, command output you want to re-read, intermediate lists, or a probe script to run against the project's code. It is never committed and is wiped when a run finishes (a failed run's scratchpad is retained for inspection until the next run starts and clears it). Paths are relative to the scratchpad and cannot reach the repository.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
path: { type: "string", description: "Path relative to the scratchpad directory" },
|
|
40
|
+
content: { type: "string", description: "Full file contents to write" },
|
|
41
|
+
},
|
|
42
|
+
required: ["path", "content"],
|
|
43
|
+
},
|
|
44
|
+
scope: { pathFields: ["path"], confineTo: SCRATCHPAD_DIR },
|
|
45
|
+
async run(input, ctx) {
|
|
46
|
+
const [target] = ctx.resolvedPaths;
|
|
47
|
+
if (target === undefined)
|
|
48
|
+
return { content: "no path supplied", isError: true };
|
|
49
|
+
const content = input.content;
|
|
50
|
+
if (typeof content !== "string")
|
|
51
|
+
return { content: "content must be a string", isError: true };
|
|
52
|
+
// Check the ceiling BEFORE any directory or file creation, so a refused
|
|
53
|
+
// write leaves no empty file at the target. This is the load-bearing
|
|
54
|
+
// property the AC11 boundary pins.
|
|
55
|
+
const size = Buffer.byteLength(content, "utf8");
|
|
56
|
+
if (size > ctx.maxFileBytes) {
|
|
57
|
+
return {
|
|
58
|
+
content: `content is ${size} bytes, which exceeds the ${ctx.maxFileBytes}-byte file ceiling`,
|
|
59
|
+
isError: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await mkdir(dirname(target), { recursive: true });
|
|
64
|
+
await writeFile(target, content, "utf8");
|
|
65
|
+
return { content: `wrote ${size} bytes` };
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return { content: err instanceof Error ? err.message : String(err), isError: true };
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const scratchpadReadTool = {
|
|
73
|
+
name: "ScratchpadRead",
|
|
74
|
+
description: "Read a throwaway file from your scratchpad at .nax/scratchpad/. Paths are relative to the scratchpad. Use it to re-read notes, command output and intermediate lists you wrote with ScratchpadWrite -- including the output a truncated tool result spilled there. Optionally pass offset (1-based line number to start from) and/or limit (maximum number of lines to return) to page through a body too large to read at once.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
path: { type: "string", description: "Path relative to the scratchpad directory" },
|
|
79
|
+
offset: { type: "integer", minimum: 1, description: "1-based line number to start reading from" },
|
|
80
|
+
limit: { type: "integer", minimum: 1, description: "Maximum number of lines to return" },
|
|
81
|
+
},
|
|
82
|
+
required: ["path"],
|
|
83
|
+
},
|
|
84
|
+
scope: { pathFields: ["path"], confineTo: SCRATCHPAD_DIR },
|
|
85
|
+
async run(input, ctx) {
|
|
86
|
+
const [target] = ctx.resolvedPaths;
|
|
87
|
+
if (target === undefined)
|
|
88
|
+
return { content: "no path supplied", isError: true };
|
|
89
|
+
const requestedPath = typeof input.path === "string" ? input.path : "<path>";
|
|
90
|
+
const rawOffset = input.offset;
|
|
91
|
+
if (rawOffset !== undefined && typeof rawOffset !== "number") {
|
|
92
|
+
return { content: "offset must be an integer", isError: true };
|
|
93
|
+
}
|
|
94
|
+
const rawLimit = input.limit;
|
|
95
|
+
if (rawLimit !== undefined && typeof rawLimit !== "number") {
|
|
96
|
+
return { content: "limit must be an integer", isError: true };
|
|
97
|
+
}
|
|
98
|
+
const offset = rawOffset;
|
|
99
|
+
const limit = rawLimit;
|
|
100
|
+
try {
|
|
101
|
+
// The tool bounds its own I/O at `readCeiling`, NOT at `maxBytes`: the
|
|
102
|
+
// model-facing cap belongs to the session's after_tool policy, which
|
|
103
|
+
// also owns the spill of whatever it cuts. Reading up to the ceiling is
|
|
104
|
+
// what lets a body in (maxBytes, readCeiling) reach that policy whole.
|
|
105
|
+
//
|
|
106
|
+
// `resultBytesPreTruncation` reports the file's FULL byte length, before
|
|
107
|
+
// any of this, so the ledger can answer "how much did we discard" for a
|
|
108
|
+
// result that was shaped after it left here.
|
|
109
|
+
const slice = await readFileSlice(target, {
|
|
110
|
+
readCeiling: ctx.readCeiling ?? READ_CEILING,
|
|
111
|
+
...(offset !== undefined ? { offset } : {}),
|
|
112
|
+
...(limit !== undefined ? { limit } : {}),
|
|
113
|
+
});
|
|
114
|
+
const fullBytes = fileSizeOrZero(target);
|
|
115
|
+
const { content, bounded, totalLines } = slice;
|
|
116
|
+
// The header leads EVERY read that returns file content, the paged one
|
|
117
|
+
// included: it reports the FILE's line count, which is what a model
|
|
118
|
+
// paging a spilled body needs in order to know how many pages remain —
|
|
119
|
+
// the page it asked for is not a substitute for that number. Precedent
|
|
120
|
+
// from readTool's whole-file read, marked with `+` when the read stopped
|
|
121
|
+
// at the I/O ceiling and the count is therefore a floor rather than the
|
|
122
|
+
// file's true total.
|
|
123
|
+
const header = `[${bounded ? `${totalLines}+` : `${totalLines}`} lines]`;
|
|
124
|
+
if (offset !== undefined || limit !== undefined) {
|
|
125
|
+
// An offset past the last line is answered with the line count rather
|
|
126
|
+
// than an empty result, because "there is nothing here" is
|
|
127
|
+
// indistinguishable from "the file is empty" and the model can act on
|
|
128
|
+
// the number -- and there the count IS the message, so no header leads
|
|
129
|
+
// it. The condition is the OFFSET against the file, never the emptiness
|
|
130
|
+
// of the slice: a range that selects only blank lines is a perfectly
|
|
131
|
+
// valid page, and reporting it as past-the-end would hide content the
|
|
132
|
+
// caller asked for and that the file really holds.
|
|
133
|
+
if (offset !== undefined && offset > totalLines) {
|
|
134
|
+
const pastEnd = `offset ${String(offset)} is past the end of the file -- it has ${totalLines} lines`;
|
|
135
|
+
return { content: pastEnd, resultBytesPreTruncation: fullBytes };
|
|
136
|
+
}
|
|
137
|
+
// The header goes on its own line and the requested range follows it,
|
|
138
|
+
// verbatim -- possibly empty, when the page holds blank lines.
|
|
139
|
+
// readTool's ranged read composes it the same way.
|
|
140
|
+
return { content: `${header}\n${content}`, resultBytesPreTruncation: fullBytes };
|
|
141
|
+
}
|
|
142
|
+
return { content: content === "" ? header : `${header}\n${content}`, resultBytesPreTruncation: fullBytes };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
// A missing file is a tool ERROR the model can react to, never a
|
|
146
|
+
// denial -- the policy already said yes. Naming the requested path is
|
|
147
|
+
// what AC7 pins: the model should see what it asked for, not "ENOENT".
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
149
|
+
return { content: `cannot read "${requestedPath}": ${message}`, isError: true };
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
export const scratchpadListTool = {
|
|
154
|
+
name: "ScratchpadList",
|
|
155
|
+
description: "List files in your scratchpad at .nax/scratchpad/. Returns paths relative to the scratchpad. Reports no entries when the scratchpad is empty or has not been written to this run.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {},
|
|
159
|
+
},
|
|
160
|
+
// No path field: there is nothing for the policy to gate per call. The
|
|
161
|
+
// directory the tool reads is the confined directory, declared at registration.
|
|
162
|
+
scope: { pathFields: [], confineTo: SCRATCHPAD_DIR },
|
|
163
|
+
async run(_input, ctx) {
|
|
164
|
+
const scratchpad = join(ctx.root, SCRATCHPAD_DIR);
|
|
165
|
+
// A missing scratchpad is not an error -- a model that has not written
|
|
166
|
+
// anything yet (or one whose prior writes have been wiped at run start)
|
|
167
|
+
// should see a clean empty listing, not an ENOENT to recover from.
|
|
168
|
+
if (!existsSync(scratchpad))
|
|
169
|
+
return { content: "(no entries)" };
|
|
170
|
+
const entries = getAgentRuntime().globSync("**/*", { cwd: scratchpad, absolute: false });
|
|
171
|
+
// Mirror glob.ts: every match is re-checked through resolveWithin before
|
|
172
|
+
// it is emitted, so a directory symlink planted inside the scratchpad
|
|
173
|
+
// cannot leak external paths the policy never saw. resolveWithin runs
|
|
174
|
+
// the symlink through isInside(realOrRaw(...)), so a symlink to outside
|
|
175
|
+
// the policy root yields null and the entry is skipped.
|
|
176
|
+
//
|
|
177
|
+
// Sort first, then cap. Capping first would leak the scan's enumeration
|
|
178
|
+
// order into the model, which is not what any caller expects from a
|
|
179
|
+
// listing -- a deterministic alphabetical prefix is the useful signal
|
|
180
|
+
// when a scratchpad has grown past the cap. The caller materializes the
|
|
181
|
+
// full list before we see it, so the sort/cap are bounded by what the
|
|
182
|
+
// scan produced, not by anything the tool itself decided to walk.
|
|
183
|
+
const sorted = [...entries].sort();
|
|
184
|
+
const files = [];
|
|
185
|
+
for (const hit of sorted) {
|
|
186
|
+
if (files.length >= MAX_MATCHES)
|
|
187
|
+
break;
|
|
188
|
+
if (resolveWithin(ctx.root, hit) === null)
|
|
189
|
+
continue;
|
|
190
|
+
files.push(hit.split(sep).join("/"));
|
|
191
|
+
}
|
|
192
|
+
if (files.length === 0)
|
|
193
|
+
return { content: "(no entries)" };
|
|
194
|
+
const rendered = files.join("\n");
|
|
195
|
+
return { content: rendered };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spill writer and the model-facing composition of the truncation policy
|
|
3
|
+
* (US-003).
|
|
4
|
+
*
|
|
5
|
+
* The policy itself (`truncateForModel`) decides *what* the model may see; this
|
|
6
|
+
* module decides what happens to the rest. On truncation only, the untruncated
|
|
7
|
+
* body — up to `READ_CEILING` — is written under the scratchpad at
|
|
8
|
+
* `spill/<toolName>-<callId>.txt`, and the marker naming that path and both byte
|
|
9
|
+
* counts is appended to the content the model receives. `writeSpill` keeps
|
|
10
|
+
* returning the scratchpad-relative spelling; `applyModelTruncationPolicy`
|
|
11
|
+
* turns it into the path the marker names — root-relative
|
|
12
|
+
* (`.nax/scratchpad/spill/...`) by default, so the session's own tools and
|
|
13
|
+
* shell can open it verbatim, or absolute when the root is not the session's
|
|
14
|
+
* shell root. The spilled body stays recoverable with `offset`/`limit`.
|
|
15
|
+
*
|
|
16
|
+
* The composition lives here, beside the writer, because the marker and the
|
|
17
|
+
* spill are one decision: the path may only appear in the marker when the write
|
|
18
|
+
* actually succeeded. Both transports reach it — the coding-tool runtime applies
|
|
19
|
+
* it to every genuine tool result, and the native session applies it at the
|
|
20
|
+
* `after_tool` chokepoint — so there is exactly one implementation of the
|
|
21
|
+
* model-facing cap rather than one per tool.
|
|
22
|
+
*
|
|
23
|
+
* Filesystem calls are injectable (`_spillDeps`), following `_scratchpadWipeDeps`
|
|
24
|
+
* in src/execution/lifecycle/scratchpad-wipe.ts, and writes go through
|
|
25
|
+
* asynchronous Node filesystem APIs.
|
|
26
|
+
*/
|
|
27
|
+
/** Directory under the scratchpad that holds spilled tool output. */
|
|
28
|
+
export declare const SPILL_DIR = "spill";
|
|
29
|
+
/** Injectable filesystem seam (see docs/architecture/conventions.md §2). */
|
|
30
|
+
export declare const _spillDeps: {
|
|
31
|
+
mkdir: (path: string) => Promise<string | undefined>;
|
|
32
|
+
writeFile: (path: string, data: string) => Promise<number>;
|
|
33
|
+
};
|
|
34
|
+
export interface SpillRequest {
|
|
35
|
+
/** Absolute root holding `.nax/scratchpad/` — the same root the tools are confined to. */
|
|
36
|
+
readonly root: string;
|
|
37
|
+
readonly toolName: string;
|
|
38
|
+
readonly callId: string;
|
|
39
|
+
/** The untruncated body, exactly as the tool returned it. */
|
|
40
|
+
readonly body: string;
|
|
41
|
+
}
|
|
42
|
+
/** `spill/<toolName>-<callId>.txt`, relative to the scratchpad directory. */
|
|
43
|
+
export declare function spillRelativePath(toolName: string, callId: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Write the untruncated body under the scratchpad and return its path relative
|
|
46
|
+
* to the scratchpad, or `undefined` when the write failed.
|
|
47
|
+
*
|
|
48
|
+
* Failure is fail-open by contract: the caller still delivers the truncated
|
|
49
|
+
* result. What it must not do is name a file that is not there, which is why
|
|
50
|
+
* this returns the path rather than a boolean.
|
|
51
|
+
*/
|
|
52
|
+
export declare function writeSpill(req: SpillRequest): Promise<string | undefined>;
|
|
53
|
+
export interface ModelTruncationOptions {
|
|
54
|
+
readonly toolName: string;
|
|
55
|
+
/** Identifies the spill file; the model's own tool-call id when it has one. */
|
|
56
|
+
readonly callId: string;
|
|
57
|
+
/** Absolute root to spill under. Omitted means no spill, and no path in the marker. */
|
|
58
|
+
readonly root?: string;
|
|
59
|
+
/** Model-facing byte ceiling. Defaults to `MODEL_MAX_BYTES`. */
|
|
60
|
+
readonly maxBytes?: number;
|
|
61
|
+
/**
|
|
62
|
+
* How the marker spells the spill path. `"root-relative"` (the default)
|
|
63
|
+
* names it from the directory the session's tools and shell start in
|
|
64
|
+
* (`.nax/scratchpad/spill/...`), so the model can open it verbatim;
|
|
65
|
+
* `"absolute"` names the written file's absolute path, for a caller whose
|
|
66
|
+
* root is not that directory (the native transcript fallback).
|
|
67
|
+
*/
|
|
68
|
+
readonly spillPathStyle?: "root-relative" | "absolute";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Apply the model-facing truncation policy to one tool result.
|
|
72
|
+
*
|
|
73
|
+
* Returns the body unchanged when it is within every cap — no spill, no marker.
|
|
74
|
+
* Otherwise the shared policy shapes the content, the untruncated body is
|
|
75
|
+
* spilled when a root is known, and the marker naming both byte counts and the
|
|
76
|
+
* spill path is composed inside the same ceilings the body obeys.
|
|
77
|
+
*
|
|
78
|
+
* The byte ceiling is the caller's `maxBytes`, defaulting to `MODEL_MAX_BYTES`;
|
|
79
|
+
* the line ceilings always come from the shared policy's constants.
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyModelTruncationPolicy(body: string, opts: ModelTruncationOptions): Promise<string>;
|