@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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared model-facing truncation policy — US-001.
|
|
3
|
+
*
|
|
4
|
+
* The single byte-safe implementation that `after_tool` will apply, replacing
|
|
5
|
+
* the per-tool `truncate(body, ctx.maxBytes)` helpers. Constants, direction
|
|
6
|
+
* lookup, and the core algorithm all live here so a future regression that
|
|
7
|
+
* resuscitates a per-tool slicer sits behind one import rather than 5+.
|
|
8
|
+
*
|
|
9
|
+
* Three independent caps compose as an ORDERED PIPELINE, never as alternatives:
|
|
10
|
+
* 1. Per-line cap: every line longer than MODEL_MAX_LINE_CHARS (UTF-16
|
|
11
|
+
* code units) is shortened.
|
|
12
|
+
* 2. Line-count cap: if still over MODEL_MAX_LINES lines, direction selects
|
|
13
|
+
* which lines to keep — over the WHOLE body, not over a byte window of it.
|
|
14
|
+
* 3. Byte cap LAST: if the result still exceeds MODEL_MAX_BYTES, cut on a
|
|
15
|
+
* codepoint boundary. Running it last is what makes the byte ceiling
|
|
16
|
+
* unconditional — nothing is appended or prepended after the cut. Under
|
|
17
|
+
* `tail-with-first-line` the first line is retained INSIDE that budget,
|
|
18
|
+
* with the trailing slice taken against what remains after the first line
|
|
19
|
+
* and its newline; if the first line alone does not fit, it is itself cut.
|
|
20
|
+
*
|
|
21
|
+
* Line counting: a trailing newline TERMINATES the last line rather than
|
|
22
|
+
* opening an empty one, and an empty body has no lines, matching
|
|
23
|
+
* `readFileSlice`'s `totalLines` and `readTool`'s `[N lines]` header.
|
|
24
|
+
*/
|
|
25
|
+
/** Tool-layer I/O bound for ranged file reads. */
|
|
26
|
+
export declare const READ_CEILING = 2000000;
|
|
27
|
+
export declare const MODEL_MAX_BYTES = 40000;
|
|
28
|
+
export declare const MODEL_MAX_LINES = 1000;
|
|
29
|
+
export declare const MODEL_MAX_LINE_CHARS = 2000;
|
|
30
|
+
export type TruncationDirection = "head" | "tail-with-first-line";
|
|
31
|
+
/** Result of `truncateForModel`: the rewritten body and whether any stage changed it. */
|
|
32
|
+
export interface TruncationResult {
|
|
33
|
+
readonly content: string;
|
|
34
|
+
readonly truncated: boolean;
|
|
35
|
+
/** Full UTF-8 byte length of the input, regardless of whether anything was truncated. */
|
|
36
|
+
readonly originalBytes: number;
|
|
37
|
+
}
|
|
38
|
+
/** Options passed to `truncateForModel`. Direction is determined by tool name via `truncationDirectionFor`. */
|
|
39
|
+
export interface TruncateForModelOptions {
|
|
40
|
+
readonly direction: TruncationDirection;
|
|
41
|
+
}
|
|
42
|
+
/** Look up the truncation direction for a tool by name. */
|
|
43
|
+
export declare function truncationDirectionFor(toolName: string): TruncationDirection;
|
|
44
|
+
/**
|
|
45
|
+
* Split a body into lines honouring the trailing-newline convention:
|
|
46
|
+
* "a\nb\n" is two lines, not three. An empty body yields zero lines.
|
|
47
|
+
*
|
|
48
|
+
* Exported because the spill-and-marker composition (src/tools/spill.ts) has
|
|
49
|
+
* to count the same lines this policy counts: a marker line occupies one of
|
|
50
|
+
* `MODEL_MAX_LINES`, and counting it with a raw `split("\n")` would let the
|
|
51
|
+
* delivered content exceed the cap by exactly the phantom empty line.
|
|
52
|
+
*/
|
|
53
|
+
export declare function splitModelLines(body: string): string[];
|
|
54
|
+
/**
|
|
55
|
+
* Shorten a single line so its UTF-16 code-unit length is at most
|
|
56
|
+
* `maxCodeUnits`. Backs up one code unit if the cut would land on a high
|
|
57
|
+
* surrogate (the lead of a surrogate pair), so the result never carries
|
|
58
|
+
* a lone surrogate that would re-encode as U+FFFD in a downstream stage.
|
|
59
|
+
*
|
|
60
|
+
* Exported for the same reason `splitModelLines` is: the marker composition
|
|
61
|
+
* keeps lines that the policy has not yet shaped, and it must shorten them
|
|
62
|
+
* the one way the policy shortens them.
|
|
63
|
+
*/
|
|
64
|
+
export declare function capModelLine(line: string, maxCodeUnits: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* Cut `body` to at most `maxBytes` bytes on a clean codepoint boundary.
|
|
67
|
+
* Nothing is appended after the cut: the byte cap is unconditional.
|
|
68
|
+
*
|
|
69
|
+
* The one codepoint-boundary slicer in the tool layer — `readFileSlice`
|
|
70
|
+
* needs the same guarantee for its own I/O bound, so it shares this rather
|
|
71
|
+
* than growing a second copy.
|
|
72
|
+
*
|
|
73
|
+
* The boundary is found STRUCTURALLY, by walking back over continuation bytes,
|
|
74
|
+
* not by measuring the decoded candidate's byte length. Measurement is unsound:
|
|
75
|
+
* a slice that ends mid-codepoint decodes into a U+FFFD, and that replacement
|
|
76
|
+
* costs 3 bytes — exactly what a truncated 4-byte codepoint contributed when
|
|
77
|
+
* three of its four bytes were included. The length check then passes while the
|
|
78
|
+
* output carries a replacement character, which is the outcome the codepoint
|
|
79
|
+
* boundary exists to prevent. Walking back stops where a codepoint starts, so
|
|
80
|
+
* the slice is always whole codepoints and re-encodes to exactly `cut` bytes.
|
|
81
|
+
*/
|
|
82
|
+
export declare function cutBufferToByteCap(buffer: Buffer, maxBytes: number): Buffer;
|
|
83
|
+
export declare function cutToByteCap(body: string, maxBytes: number): string;
|
|
84
|
+
/**
|
|
85
|
+
* Apply the model-facing truncation policy to a tool result body.
|
|
86
|
+
*
|
|
87
|
+
* Three stages, in order, each independently reachable: per-line cap,
|
|
88
|
+
* line-count cap, byte cap. The pipeline reports `truncated: true` when
|
|
89
|
+
* any stage changed the content. `originalBytes` always reports the
|
|
90
|
+
* input's full UTF-8 byte length, regardless of which stages fired.
|
|
91
|
+
*
|
|
92
|
+
* Direction governs both stages that drop content: the line-count cap keeps
|
|
93
|
+
* the first N lines (`head`) or the first line plus the last N-1 lines
|
|
94
|
+
* (`tail-with-first-line`), and the byte cap keeps the leading bytes or the
|
|
95
|
+
* first line plus the trailing bytes the same way. Within-cap bodies are
|
|
96
|
+
* returned unchanged regardless of direction.
|
|
97
|
+
*/
|
|
98
|
+
export declare function truncateForModel(body: string, opts: TruncateForModelOptions): TruncationResult;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared model-facing truncation policy — US-001.
|
|
3
|
+
*
|
|
4
|
+
* The single byte-safe implementation that `after_tool` will apply, replacing
|
|
5
|
+
* the per-tool `truncate(body, ctx.maxBytes)` helpers. Constants, direction
|
|
6
|
+
* lookup, and the core algorithm all live here so a future regression that
|
|
7
|
+
* resuscitates a per-tool slicer sits behind one import rather than 5+.
|
|
8
|
+
*
|
|
9
|
+
* Three independent caps compose as an ORDERED PIPELINE, never as alternatives:
|
|
10
|
+
* 1. Per-line cap: every line longer than MODEL_MAX_LINE_CHARS (UTF-16
|
|
11
|
+
* code units) is shortened.
|
|
12
|
+
* 2. Line-count cap: if still over MODEL_MAX_LINES lines, direction selects
|
|
13
|
+
* which lines to keep — over the WHOLE body, not over a byte window of it.
|
|
14
|
+
* 3. Byte cap LAST: if the result still exceeds MODEL_MAX_BYTES, cut on a
|
|
15
|
+
* codepoint boundary. Running it last is what makes the byte ceiling
|
|
16
|
+
* unconditional — nothing is appended or prepended after the cut. Under
|
|
17
|
+
* `tail-with-first-line` the first line is retained INSIDE that budget,
|
|
18
|
+
* with the trailing slice taken against what remains after the first line
|
|
19
|
+
* and its newline; if the first line alone does not fit, it is itself cut.
|
|
20
|
+
*
|
|
21
|
+
* Line counting: a trailing newline TERMINATES the last line rather than
|
|
22
|
+
* opening an empty one, and an empty body has no lines, matching
|
|
23
|
+
* `readFileSlice`'s `totalLines` and `readTool`'s `[N lines]` header.
|
|
24
|
+
*/
|
|
25
|
+
/** Tool-layer I/O bound for ranged file reads. */
|
|
26
|
+
export const READ_CEILING = 2_000_000;
|
|
27
|
+
export const MODEL_MAX_BYTES = 40_000;
|
|
28
|
+
export const MODEL_MAX_LINES = 1_000;
|
|
29
|
+
export const MODEL_MAX_LINE_CHARS = 2_000;
|
|
30
|
+
/** Tools whose output direction is `tail-with-first-line` — keep the start AND the end. */
|
|
31
|
+
const TAIL_DIRECTION_TOOLS = new Set(["Bash", "RunCommand", "Exec"]);
|
|
32
|
+
/** Look up the truncation direction for a tool by name. */
|
|
33
|
+
export function truncationDirectionFor(toolName) {
|
|
34
|
+
if (TAIL_DIRECTION_TOOLS.has(toolName))
|
|
35
|
+
return "tail-with-first-line";
|
|
36
|
+
return "head";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Split a body into lines honouring the trailing-newline convention:
|
|
40
|
+
* "a\nb\n" is two lines, not three. An empty body yields zero lines.
|
|
41
|
+
*
|
|
42
|
+
* Exported because the spill-and-marker composition (src/tools/spill.ts) has
|
|
43
|
+
* to count the same lines this policy counts: a marker line occupies one of
|
|
44
|
+
* `MODEL_MAX_LINES`, and counting it with a raw `split("\n")` would let the
|
|
45
|
+
* delivered content exceed the cap by exactly the phantom empty line.
|
|
46
|
+
*/
|
|
47
|
+
export function splitModelLines(body) {
|
|
48
|
+
if (body === "")
|
|
49
|
+
return [];
|
|
50
|
+
const trimmed = body.endsWith("\n") ? body.slice(0, -1) : body;
|
|
51
|
+
return trimmed.split("\n");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Shorten a single line so its UTF-16 code-unit length is at most
|
|
55
|
+
* `maxCodeUnits`. Backs up one code unit if the cut would land on a high
|
|
56
|
+
* surrogate (the lead of a surrogate pair), so the result never carries
|
|
57
|
+
* a lone surrogate that would re-encode as U+FFFD in a downstream stage.
|
|
58
|
+
*
|
|
59
|
+
* Exported for the same reason `splitModelLines` is: the marker composition
|
|
60
|
+
* keeps lines that the policy has not yet shaped, and it must shorten them
|
|
61
|
+
* the one way the policy shortens them.
|
|
62
|
+
*/
|
|
63
|
+
export function capModelLine(line, maxCodeUnits) {
|
|
64
|
+
if (line.length <= maxCodeUnits)
|
|
65
|
+
return line;
|
|
66
|
+
let cut = maxCodeUnits;
|
|
67
|
+
// Avoid splitting a surrogate pair: if the last code unit kept is a
|
|
68
|
+
// high surrogate, back up one so the pair stays whole.
|
|
69
|
+
if (cut > 0) {
|
|
70
|
+
const last = line.charCodeAt(cut - 1);
|
|
71
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
72
|
+
cut -= 1;
|
|
73
|
+
}
|
|
74
|
+
return line.slice(0, cut);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Apply the per-line cap (stage 1) to every line, preserving the array
|
|
78
|
+
* shape so later stages can count and slice by line.
|
|
79
|
+
*/
|
|
80
|
+
function applyLineCharCap(lines, maxLineChars) {
|
|
81
|
+
let changed = false;
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (line.length <= maxLineChars) {
|
|
85
|
+
out.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
changed = true;
|
|
89
|
+
out.push(capModelLine(line, maxLineChars));
|
|
90
|
+
}
|
|
91
|
+
return { lines: out, changed };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Apply the line-count cap (stage 2) by direction, chosen over the WHOLE
|
|
95
|
+
* body rather than over a byte window of it. `head` keeps the first N
|
|
96
|
+
* lines and drops the rest; `tail-with-first-line` keeps the first line
|
|
97
|
+
* followed by the last N-1 lines, dropping the middle.
|
|
98
|
+
*/
|
|
99
|
+
function applyLineCountCap(lines, maxLines, direction) {
|
|
100
|
+
if (lines.length <= maxLines)
|
|
101
|
+
return { lines, changed: false };
|
|
102
|
+
if (direction === "head") {
|
|
103
|
+
return { lines: lines.slice(0, maxLines), changed: true };
|
|
104
|
+
}
|
|
105
|
+
// tail-with-first-line: first line + last (maxLines - 1) lines.
|
|
106
|
+
// When maxLines is 0 or 1 the result is just the first line.
|
|
107
|
+
const tailCount = Math.max(0, maxLines - 1);
|
|
108
|
+
const tail = lines.slice(-tailCount);
|
|
109
|
+
const head = lines.slice(0, 1);
|
|
110
|
+
return { lines: [...head, ...tail], changed: true };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* True when `byte` is a UTF-8 continuation byte (`10xxxxxx`) — meaning the
|
|
114
|
+
* position it sits at is INSIDE a multi-byte codepoint, not at its start.
|
|
115
|
+
*/
|
|
116
|
+
function isContinuationByte(byte) {
|
|
117
|
+
return byte !== undefined && (byte & 0xc0) === 0x80;
|
|
118
|
+
}
|
|
119
|
+
function utf8SequenceLength(byte) {
|
|
120
|
+
if (byte === undefined || byte < 0x80)
|
|
121
|
+
return 1;
|
|
122
|
+
if ((byte & 0xe0) === 0xc0)
|
|
123
|
+
return 2;
|
|
124
|
+
if ((byte & 0xf0) === 0xe0)
|
|
125
|
+
return 3;
|
|
126
|
+
if ((byte & 0xf8) === 0xf0)
|
|
127
|
+
return 4;
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Cut `body` to at most `maxBytes` bytes on a clean codepoint boundary.
|
|
132
|
+
* Nothing is appended after the cut: the byte cap is unconditional.
|
|
133
|
+
*
|
|
134
|
+
* The one codepoint-boundary slicer in the tool layer — `readFileSlice`
|
|
135
|
+
* needs the same guarantee for its own I/O bound, so it shares this rather
|
|
136
|
+
* than growing a second copy.
|
|
137
|
+
*
|
|
138
|
+
* The boundary is found STRUCTURALLY, by walking back over continuation bytes,
|
|
139
|
+
* not by measuring the decoded candidate's byte length. Measurement is unsound:
|
|
140
|
+
* a slice that ends mid-codepoint decodes into a U+FFFD, and that replacement
|
|
141
|
+
* costs 3 bytes — exactly what a truncated 4-byte codepoint contributed when
|
|
142
|
+
* three of its four bytes were included. The length check then passes while the
|
|
143
|
+
* output carries a replacement character, which is the outcome the codepoint
|
|
144
|
+
* boundary exists to prevent. Walking back stops where a codepoint starts, so
|
|
145
|
+
* the slice is always whole codepoints and re-encodes to exactly `cut` bytes.
|
|
146
|
+
*/
|
|
147
|
+
export function cutBufferToByteCap(buffer, maxBytes) {
|
|
148
|
+
let cut = Math.min(buffer.length, maxBytes);
|
|
149
|
+
while (cut > 0 && isContinuationByte(buffer[cut]))
|
|
150
|
+
cut -= 1;
|
|
151
|
+
let sequenceStart = cut - 1;
|
|
152
|
+
while (sequenceStart >= 0 && isContinuationByte(buffer[sequenceStart]))
|
|
153
|
+
sequenceStart -= 1;
|
|
154
|
+
if (sequenceStart >= 0 && sequenceStart + utf8SequenceLength(buffer[sequenceStart]) > cut) {
|
|
155
|
+
cut = sequenceStart;
|
|
156
|
+
}
|
|
157
|
+
return buffer.subarray(0, cut);
|
|
158
|
+
}
|
|
159
|
+
export function cutToByteCap(body, maxBytes) {
|
|
160
|
+
const buffer = Buffer.from(body, "utf8");
|
|
161
|
+
return cutBufferToByteCap(buffer, maxBytes).toString("utf8");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Cut `body` down to its LAST `maxBytes` bytes, on a clean codepoint boundary.
|
|
165
|
+
* The mirror of `cutToByteCap`: that one backs up over the continuation bytes a
|
|
166
|
+
* cut-inside-a-codepoint would end on, this one skips forward over the
|
|
167
|
+
* continuation bytes such a start would begin on. Same structural rule, same
|
|
168
|
+
* reason — measuring the decoded candidate would accept a tail that opens with
|
|
169
|
+
* a U+FFFD whenever the replacement costs no more bytes than the partial
|
|
170
|
+
* codepoint it replaced.
|
|
171
|
+
*/
|
|
172
|
+
function tailWithinBytes(body, maxBytes) {
|
|
173
|
+
const buf = Buffer.from(body, "utf8");
|
|
174
|
+
if (buf.length <= maxBytes)
|
|
175
|
+
return body;
|
|
176
|
+
let start = buf.length - maxBytes;
|
|
177
|
+
while (start < buf.length && isContinuationByte(buf[start]))
|
|
178
|
+
start += 1;
|
|
179
|
+
return buf.subarray(start).toString("utf8");
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Cut a `tail-with-first-line` body to `maxBytes`: the body's first line, then
|
|
183
|
+
* its LAST bytes, dropping the middle. The byte stage's half of the same choice
|
|
184
|
+
* the line-count stage makes — a body cut for length should lose its middle,
|
|
185
|
+
* not its end, when the direction says the end is what matters.
|
|
186
|
+
*
|
|
187
|
+
* The first line is retained INSIDE the budget, not on top of it: the trailing
|
|
188
|
+
* slice is measured against what remains once the first line and its newline
|
|
189
|
+
* are paid for. If the first line alone does not fit, it is itself cut. Either
|
|
190
|
+
* way nothing is appended after the cut — the trailing slice is what fills the
|
|
191
|
+
* remaining budget, never an extra.
|
|
192
|
+
*/
|
|
193
|
+
function cutKeepingFirstLine(body, maxBytes) {
|
|
194
|
+
const lineEnd = body.indexOf("\n");
|
|
195
|
+
// No newline: the body is one line, so there is no tail distinct from its head.
|
|
196
|
+
if (lineEnd === -1)
|
|
197
|
+
return cutToByteCap(body, maxBytes);
|
|
198
|
+
const firstLine = body.slice(0, lineEnd);
|
|
199
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf8");
|
|
200
|
+
// Paying for the first line and its newline leaves no room for a tail.
|
|
201
|
+
if (firstLineBytes + 1 >= maxBytes)
|
|
202
|
+
return cutToByteCap(firstLine, maxBytes);
|
|
203
|
+
const tail = tailWithinBytes(body.slice(lineEnd + 1), maxBytes - firstLineBytes - 1);
|
|
204
|
+
return `${firstLine}\n${tail}`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Apply the model-facing truncation policy to a tool result body.
|
|
208
|
+
*
|
|
209
|
+
* Three stages, in order, each independently reachable: per-line cap,
|
|
210
|
+
* line-count cap, byte cap. The pipeline reports `truncated: true` when
|
|
211
|
+
* any stage changed the content. `originalBytes` always reports the
|
|
212
|
+
* input's full UTF-8 byte length, regardless of which stages fired.
|
|
213
|
+
*
|
|
214
|
+
* Direction governs both stages that drop content: the line-count cap keeps
|
|
215
|
+
* the first N lines (`head`) or the first line plus the last N-1 lines
|
|
216
|
+
* (`tail-with-first-line`), and the byte cap keeps the leading bytes or the
|
|
217
|
+
* first line plus the trailing bytes the same way. Within-cap bodies are
|
|
218
|
+
* returned unchanged regardless of direction.
|
|
219
|
+
*/
|
|
220
|
+
export function truncateForModel(body, opts) {
|
|
221
|
+
const originalBytes = Buffer.byteLength(body, "utf8");
|
|
222
|
+
// Split the body into lines once; every cap is checked against the
|
|
223
|
+
// line-aware view, so we don't pay for `split("\n")` three times.
|
|
224
|
+
const lines = splitModelLines(body);
|
|
225
|
+
// Per-line cap is measured in UTF-16 code units — the same metric
|
|
226
|
+
// `String#length` reports — so a 2_000-character line of single-unit
|
|
227
|
+
// codepoints is at the cap, not over it. It is independent of the byte
|
|
228
|
+
// ceiling: stage 3 alone decides which otherwise-valid bytes are retained.
|
|
229
|
+
const perLineOver = lines.some((l) => l.length > MODEL_MAX_LINE_CHARS);
|
|
230
|
+
let working = lines;
|
|
231
|
+
let changed = false;
|
|
232
|
+
// Stage 1: per-line cap.
|
|
233
|
+
if (perLineOver) {
|
|
234
|
+
const capped = applyLineCharCap(working, MODEL_MAX_LINE_CHARS);
|
|
235
|
+
working = capped.lines;
|
|
236
|
+
if (capped.changed)
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
// Stage 2: line-count cap. The cap fires only when the body is strictly
|
|
240
|
+
// over MODEL_MAX_LINES — within-cap bodies must be returned unchanged
|
|
241
|
+
// per AC1, so no preview is allowed at smaller scales.
|
|
242
|
+
if (working.length > MODEL_MAX_LINES) {
|
|
243
|
+
const trimmed = applyLineCountCap(working, MODEL_MAX_LINES, opts.direction);
|
|
244
|
+
working = trimmed.lines;
|
|
245
|
+
if (trimmed.changed)
|
|
246
|
+
changed = true;
|
|
247
|
+
}
|
|
248
|
+
// Re-join with newlines. The trailing-newline convention: the "\n" is
|
|
249
|
+
// preserved only when the body ended in one and NO stage modified it —
|
|
250
|
+
// `changed` covers a line merely SHORTENED by stage 1 as well as one
|
|
251
|
+
// DROPPED by stage 2. Gating on "dropped" alone would be wrong: stage 2
|
|
252
|
+
// fires only at strictly more than MODEL_MAX_LINES, so a stage-1-shortened
|
|
253
|
+
// body can sit at exactly MODEL_MAX_LINES lines, and re-emitting "\n" there
|
|
254
|
+
// inflates `split("\n").length` to MODEL_MAX_LINES + 1 — past the cap that
|
|
255
|
+
// AC4 pins via the naive split. The conservative guard is deliberate: a
|
|
256
|
+
// modified body has already lost byte-for-byte fidelity, so dropping its
|
|
257
|
+
// terminator costs nothing the caller can observe, while re-adding it can
|
|
258
|
+
// break the count.
|
|
259
|
+
const joined = working.join("\n");
|
|
260
|
+
let rebuilt = !changed && body.endsWith("\n") && working.length > 0 ? `${joined}\n` : joined;
|
|
261
|
+
// Stage 3: byte cap. Runs last, so the byte ceiling is unconditional —
|
|
262
|
+
// nothing is appended after the cut. The direction decides which bytes
|
|
263
|
+
// survive the cut, exactly as it decided which lines survived stage 2:
|
|
264
|
+
// `head` keeps the leading bytes, `tail-with-first-line` keeps the first
|
|
265
|
+
// line plus the trailing bytes and drops the middle.
|
|
266
|
+
let truncated = changed;
|
|
267
|
+
if (Buffer.byteLength(rebuilt, "utf8") > MODEL_MAX_BYTES) {
|
|
268
|
+
rebuilt =
|
|
269
|
+
opts.direction === "tail-with-first-line"
|
|
270
|
+
? cutKeepingFirstLine(rebuilt, MODEL_MAX_BYTES)
|
|
271
|
+
: cutToByteCap(rebuilt, MODEL_MAX_BYTES);
|
|
272
|
+
truncated = true;
|
|
273
|
+
}
|
|
274
|
+
// Trim a trailing surrogate so the byte cap (or the per-line cap) cannot
|
|
275
|
+
// leave the body ending on a lone half of a surrogate pair. A cut that
|
|
276
|
+
// lands on a UTF-16 surrogate code unit is a structural artifact of the
|
|
277
|
+
// cap — there is no scenario in which ending the body on a surrogate is
|
|
278
|
+
// intentional, and downstream UTF-8 decoding of such a body produces a
|
|
279
|
+
// U+FFFD replacement character that the model cannot distinguish from
|
|
280
|
+
// a real codepoint. We only strip when something above changed the
|
|
281
|
+
// content; an untruncated body that happens to end on a surrogate is
|
|
282
|
+
// already what the caller gave us and is left alone.
|
|
283
|
+
if (truncated && rebuilt.length > 0) {
|
|
284
|
+
const lastChar = rebuilt.charCodeAt(rebuilt.length - 1);
|
|
285
|
+
if (lastChar >= 0xd800 && lastChar <= 0xdfff) {
|
|
286
|
+
let trimmed = rebuilt.length;
|
|
287
|
+
while (trimmed > 0) {
|
|
288
|
+
const c = rebuilt.charCodeAt(trimmed - 1);
|
|
289
|
+
if (c < 0xd800 || c > 0xdfff)
|
|
290
|
+
break;
|
|
291
|
+
trimmed -= 1;
|
|
292
|
+
}
|
|
293
|
+
rebuilt = rebuilt.slice(0, trimmed);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { content: rebuilt, truncated, originalBytes };
|
|
297
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for nax's own coding tools.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately free of any transport type: this module is imported by the
|
|
5
|
+
* policy, the tools and the runtime, none of which may see `@nathapp/nax-ai`
|
|
6
|
+
* (check:nax-ai-imports confines that package to src/agents/native/).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Policy identity for RunCommand's allowlisted, model-authored argv branch,
|
|
10
|
+
* and the marker an operation's `tools` declaration uses to request it.
|
|
11
|
+
* `Exec` is not a registered tool — nothing in `registerBuiltinCodingTools`
|
|
12
|
+
* carries this name, and it is reserved (`RESERVED_TOOL_NAMES`) precisely so
|
|
13
|
+
* no third party can register a tool that shadows the identity.
|
|
14
|
+
*/
|
|
15
|
+
export declare const EXEC_TOOL_NAME = "Exec";
|
|
16
|
+
/**
|
|
17
|
+
* Policy identity of the model-authored shell tool.
|
|
18
|
+
*
|
|
19
|
+
* Unlike `Exec`, `Bash` IS a registered tool — but a session-local one
|
|
20
|
+
* (`createBashTool`), because it needs the project's shell and env-strip list.
|
|
21
|
+
* Reserved (`RESERVED_TOOL_NAMES`) so no third party can register a tool that
|
|
22
|
+
* shadows the identity the policy gates.
|
|
23
|
+
*/
|
|
24
|
+
export declare const BASH_TOOL_NAME = "Bash";
|
|
25
|
+
/** The tools nax ships. Third parties register additional names at runtime. */
|
|
26
|
+
export type CodingToolName = "Read" | "Glob" | "Grep" | "Write" | "Edit" | "Delete" | "Git" | "GitCommit" | "RunCommand" | "RequestCapability" | "Exec" | "Bash" | "ScratchpadWrite" | "ScratchpadRead" | "ScratchpadList";
|
|
27
|
+
/**
|
|
28
|
+
* One declarative permission grant, as produced by resolvePermissions.
|
|
29
|
+
*
|
|
30
|
+
* `patterns` is either globs over the tool's path-bearing fields
|
|
31
|
+
* (`["src/**"]`), or the verb list for a verb-gated tool (`["diff","log"]`).
|
|
32
|
+
* `["*"]` means unconditional — but never wider than the root.
|
|
33
|
+
*/
|
|
34
|
+
export interface ToolGrant {
|
|
35
|
+
readonly tool: string;
|
|
36
|
+
readonly patterns: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* How the policy gates a given tool, declared by the tool itself.
|
|
40
|
+
*
|
|
41
|
+
* Declaring the path-bearing fields is what lets the policy gate a tool it has
|
|
42
|
+
* no special knowledge of, including one registered by a third party. A tool
|
|
43
|
+
* with no path fields is gated at the tool/verb level instead — the honest
|
|
44
|
+
* expression for something whose arguments are not paths.
|
|
45
|
+
*
|
|
46
|
+
* `arrayPathFields` and `refPathFields` extend the same containment seam to
|
|
47
|
+
* array-valued inputs — Git is the only current user.
|
|
48
|
+
*
|
|
49
|
+
* A verb-gated tool's grant list is overloaded: `["diff","log"]` are verbs, so
|
|
50
|
+
* matching them against a path denies everything. The policy separates the two
|
|
51
|
+
* by `allowedVerbs`, a closed set the tool declares — a pattern naming a
|
|
52
|
+
* permitted verb is a verb, anything else is a path glob. So `Git(diff,src/**)`
|
|
53
|
+
* grants the `diff` verb over `src/**`, and `Git(diff)` declares no path glob
|
|
54
|
+
* and is bounded by the root alone.
|
|
55
|
+
*/
|
|
56
|
+
export interface ToolScope {
|
|
57
|
+
/**
|
|
58
|
+
* String paths in the input. Dot-separated names address nested object
|
|
59
|
+
* properties (for example `values.files` in RunCommand).
|
|
60
|
+
*/
|
|
61
|
+
readonly pathFields: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* Root-relative subdirectory the tool's path fields are confined to.
|
|
64
|
+
* Containment runs against `<root>/<confineTo>` instead of `<root>`, so a
|
|
65
|
+
* path-bearing tool can never reach the rest of the repository even under
|
|
66
|
+
* an unconditional grant. Grant globs, deny rules and `naxOwnedWriteRefusal`
|
|
67
|
+
* keep seeing the canonical repo-root-relative spelling: containment is the
|
|
68
|
+
* single seam, only its ROOT shifts. Tool-declared, never config-declared --
|
|
69
|
+
* a mis-set `pathFields` grant must not be able to widen the tool's reach.
|
|
70
|
+
*/
|
|
71
|
+
readonly confineTo?: string;
|
|
72
|
+
/** Array-valued fields whose every element is a path. */
|
|
73
|
+
readonly arrayPathFields?: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* String fields whose value is a whitespace-separated LIST of paths, every
|
|
76
|
+
* element resolved and grant-checked on its own (`splitPathList`).
|
|
77
|
+
*
|
|
78
|
+
* Distinct from `pathFields`, which checks the value as ONE path. That is
|
|
79
|
+
* sound only while the value also REACHES the shell as one argument: a
|
|
80
|
+
* multi-element value like `"a.test.ts ../../etc/passwd"` resolves, as a
|
|
81
|
+
* single path, to a location inside the root, so `pathFields` approves it.
|
|
82
|
+
* Whole-value quoting is what kept that honest -- and whole-value quoting is
|
|
83
|
+
* exactly the defect #1998 fixes. So per-element quoting and per-element
|
|
84
|
+
* checking have to land together: this field is the checking half. Nothing
|
|
85
|
+
* was exploitable before; splitting without it would have made it so.
|
|
86
|
+
*/
|
|
87
|
+
readonly listPathFields?: readonly string[];
|
|
88
|
+
/**
|
|
89
|
+
* Array-valued fields whose elements are refs that MAY carry a path after a
|
|
90
|
+
* `:` (git's `<rev>:<path>` syntax). Only the substring after the first `:`
|
|
91
|
+
* is checked for containment; an element with no `:`, or an empty path
|
|
92
|
+
* after it, is a pure revision and is left unchecked.
|
|
93
|
+
*/
|
|
94
|
+
readonly refPathFields?: readonly string[];
|
|
95
|
+
readonly verbField?: string;
|
|
96
|
+
readonly allowedVerbs?: readonly string[];
|
|
97
|
+
/**
|
|
98
|
+
* The input field holding a model-authored argv (RunCommand's `Exec`
|
|
99
|
+
* branch). When a call carries this field, the policy checks it under the
|
|
100
|
+
* `Exec` identity (`EXEC_TOOL_NAME`), matching each grant pattern against
|
|
101
|
+
* the argv token-by-token, rather than under the tool's own name — an
|
|
102
|
+
* `Exec(...)` grant is otherwise never consulted and a `RunCommand(*)`
|
|
103
|
+
* wildcard would silently cover the argv branch too.
|
|
104
|
+
*/
|
|
105
|
+
readonly argvField?: string;
|
|
106
|
+
/**
|
|
107
|
+
* The input field holding a model-authored shell COMMAND STRING (`Bash`).
|
|
108
|
+
* A call carrying this field is evaluated per shell segment by
|
|
109
|
+
* `src/tools/policy-bash.ts` rather than by the verb or path branches; a
|
|
110
|
+
* tool declaring it must be granted explicitly, since no profile grants one.
|
|
111
|
+
*/
|
|
112
|
+
readonly commandField?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `breach` separates "you may not write there" from "that path is not in this
|
|
116
|
+
* repository at all". Both deny; only the latter is logged at warn, because a
|
|
117
|
+
* path escaping the root can mean prompt injection.
|
|
118
|
+
*
|
|
119
|
+
* `outcome` makes the refusal three-state. "ask" means every OTHER gate passed
|
|
120
|
+
* and an ask rule matched -- an AskResolver may approve it (spec R1). Absent or
|
|
121
|
+
* "denied" is final. The ask shape is still `allowed: false` on purpose: any
|
|
122
|
+
* consumer that only reads `allowed` fails closed. `resolvedPaths` is present
|
|
123
|
+
* only with "ask", describing what an approval would admit.
|
|
124
|
+
*/
|
|
125
|
+
export type PolicyVerdict = {
|
|
126
|
+
readonly allowed: true;
|
|
127
|
+
readonly resolvedPaths: readonly string[];
|
|
128
|
+
} | {
|
|
129
|
+
readonly allowed: false;
|
|
130
|
+
readonly reason: string;
|
|
131
|
+
readonly breach: boolean;
|
|
132
|
+
readonly outcome?: "denied" | "ask";
|
|
133
|
+
readonly resolvedPaths?: readonly string[];
|
|
134
|
+
/** Present only with outcome "ask": the matching configured rule expression. */
|
|
135
|
+
readonly rule?: string;
|
|
136
|
+
/** See BashCheck.escalatable in policy-bash.ts. `false` on every
|
|
137
|
+
* non-escalatable denial (the shared deny helper materializes it on
|
|
138
|
+
* every branch), which reads as not-escalatable. */
|
|
139
|
+
readonly escalatable?: boolean;
|
|
140
|
+
};
|
|
141
|
+
export interface ToolPolicy {
|
|
142
|
+
readonly root: string;
|
|
143
|
+
grantedTools(): readonly string[];
|
|
144
|
+
check(tool: string, scope: ToolScope, input: Record<string, unknown>): PolicyVerdict;
|
|
145
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for nax's own coding tools.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately free of any transport type: this module is imported by the
|
|
5
|
+
* policy, the tools and the runtime, none of which may see `@nathapp/nax-ai`
|
|
6
|
+
* (check:nax-ai-imports confines that package to src/agents/native/).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Policy identity for RunCommand's allowlisted, model-authored argv branch,
|
|
10
|
+
* and the marker an operation's `tools` declaration uses to request it.
|
|
11
|
+
* `Exec` is not a registered tool — nothing in `registerBuiltinCodingTools`
|
|
12
|
+
* carries this name, and it is reserved (`RESERVED_TOOL_NAMES`) precisely so
|
|
13
|
+
* no third party can register a tool that shadows the identity.
|
|
14
|
+
*/
|
|
15
|
+
export const EXEC_TOOL_NAME = "Exec";
|
|
16
|
+
/**
|
|
17
|
+
* Policy identity of the model-authored shell tool.
|
|
18
|
+
*
|
|
19
|
+
* Unlike `Exec`, `Bash` IS a registered tool — but a session-local one
|
|
20
|
+
* (`createBashTool`), because it needs the project's shell and env-strip list.
|
|
21
|
+
* Reserved (`RESERVED_TOOL_NAMES`) so no third party can register a tool that
|
|
22
|
+
* shadows the identity the policy gates.
|
|
23
|
+
*/
|
|
24
|
+
export const BASH_TOOL_NAME = "Bash";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extra clause `policy.ts`'s `verbBranch` appends to a subcommand denial
|
|
3
|
+
* when the tool also offers an argv escape hatch (`scope.argvField`).
|
|
4
|
+
*
|
|
5
|
+
* run-2026-09-14T05-55-54-734Z, Shape B: an agent with argv available still
|
|
6
|
+
* stuffed raw shell into the verb field repeatedly rather than ever trying
|
|
7
|
+
* "argv" -- so the structural fact ("this field is never a shell string") is
|
|
8
|
+
* stated unconditionally. But a bare "use argv instead" would have sent
|
|
9
|
+
* every one of the audit's four real denials straight into a SECOND one:
|
|
10
|
+
* the compiled Exec grant was install-only (`bun install`, `bun add*`, ...)
|
|
11
|
+
* against requests like "bun test ... | head -200" and "wc". So the actual
|
|
12
|
+
* compiled grant is named here too, using the identical text
|
|
13
|
+
* `describeExecAllowlist` already renders into the tool description, which
|
|
14
|
+
* is what lets an agent see in one denial whether argv would help at all
|
|
15
|
+
* before spending a second turn finding out.
|
|
16
|
+
*
|
|
17
|
+
* Extracted from `policy.ts` into its own module so that file (already at
|
|
18
|
+
* its 600-line limit) can carry this explanation without trimming it.
|
|
19
|
+
*
|
|
20
|
+
* The clause names `argvField` explicitly (for "argv", ...) rather than
|
|
21
|
+
* appending `describeExecAllowlist`'s bare "permitted forms: ..." after the
|
|
22
|
+
* structural sentence: two unlabelled "permitted ..." lists back to back --
|
|
23
|
+
* one for `verbField`'s enum, one for argv's grant -- reads as a single
|
|
24
|
+
* list, and an agent can misread the second half as more legal values for
|
|
25
|
+
* `verbField`, denying again on the enum (the very second-denial loop this
|
|
26
|
+
* exists to prevent). `describeExecAllowlist`'s own text is unchanged: the
|
|
27
|
+
* tool description's surrounding sentence already establishes argv as the
|
|
28
|
+
* subject there, so this supplies its own framing around the same list
|
|
29
|
+
* rather than editing shared text to fit one caller. The framing is a bare
|
|
30
|
+
* attribution ("for \"argv\", ...") precisely so it composes with whichever
|
|
31
|
+
* of describeExecAllowlist's three forms is returned -- prefixing a verb
|
|
32
|
+
* such as "accepts:" instead would double the label on the populated form
|
|
33
|
+
* ("accepts: permitted forms: ...") and misread the empty one.
|
|
34
|
+
*/
|
|
35
|
+
export declare function argvShellHint(verbField: string, argvField: string | undefined, execPatterns: readonly string[]): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describeExecAllowlist } from "./exec-allowlist-text.js";
|
|
2
|
+
/**
|
|
3
|
+
* The extra clause `policy.ts`'s `verbBranch` appends to a subcommand denial
|
|
4
|
+
* when the tool also offers an argv escape hatch (`scope.argvField`).
|
|
5
|
+
*
|
|
6
|
+
* run-2026-09-14T05-55-54-734Z, Shape B: an agent with argv available still
|
|
7
|
+
* stuffed raw shell into the verb field repeatedly rather than ever trying
|
|
8
|
+
* "argv" -- so the structural fact ("this field is never a shell string") is
|
|
9
|
+
* stated unconditionally. But a bare "use argv instead" would have sent
|
|
10
|
+
* every one of the audit's four real denials straight into a SECOND one:
|
|
11
|
+
* the compiled Exec grant was install-only (`bun install`, `bun add*`, ...)
|
|
12
|
+
* against requests like "bun test ... | head -200" and "wc". So the actual
|
|
13
|
+
* compiled grant is named here too, using the identical text
|
|
14
|
+
* `describeExecAllowlist` already renders into the tool description, which
|
|
15
|
+
* is what lets an agent see in one denial whether argv would help at all
|
|
16
|
+
* before spending a second turn finding out.
|
|
17
|
+
*
|
|
18
|
+
* Extracted from `policy.ts` into its own module so that file (already at
|
|
19
|
+
* its 600-line limit) can carry this explanation without trimming it.
|
|
20
|
+
*
|
|
21
|
+
* The clause names `argvField` explicitly (for "argv", ...) rather than
|
|
22
|
+
* appending `describeExecAllowlist`'s bare "permitted forms: ..." after the
|
|
23
|
+
* structural sentence: two unlabelled "permitted ..." lists back to back --
|
|
24
|
+
* one for `verbField`'s enum, one for argv's grant -- reads as a single
|
|
25
|
+
* list, and an agent can misread the second half as more legal values for
|
|
26
|
+
* `verbField`, denying again on the enum (the very second-denial loop this
|
|
27
|
+
* exists to prevent). `describeExecAllowlist`'s own text is unchanged: the
|
|
28
|
+
* tool description's surrounding sentence already establishes argv as the
|
|
29
|
+
* subject there, so this supplies its own framing around the same list
|
|
30
|
+
* rather than editing shared text to fit one caller. The framing is a bare
|
|
31
|
+
* attribution ("for \"argv\", ...") precisely so it composes with whichever
|
|
32
|
+
* of describeExecAllowlist's three forms is returned -- prefixing a verb
|
|
33
|
+
* such as "accepts:" instead would double the label on the populated form
|
|
34
|
+
* ("accepts: permitted forms: ...") and misread the empty one.
|
|
35
|
+
*/
|
|
36
|
+
export function argvShellHint(verbField, argvField, execPatterns) {
|
|
37
|
+
if (argvField === undefined)
|
|
38
|
+
return "";
|
|
39
|
+
return ` -- "${verbField}" never takes a shell string -- for "${argvField}", ${describeExecAllowlist(execPatterns)}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file whose path the policy already resolved and approved.
|
|
3
|
+
*
|
|
4
|
+
* No production op declares this tool: everything that writes also needs to run
|
|
5
|
+
* tests, which needs Bash, which C1 excludes. It exists so #374's gate has a
|
|
6
|
+
* concrete subject (design section 3.5) and is exercised by tests alone.
|
|
7
|
+
*/
|
|
8
|
+
import type { CodingTool } from "./registry.ts";
|
|
9
|
+
export declare const writeTool: CodingTool;
|