@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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern compilation for the tool policy: globs, argv tokens, and the
|
|
3
|
+
* deny/ask rule maps.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `policy.ts` to keep that file under the 600-line source
|
|
6
|
+
* limit. `src/` reaches this module only through `policy.ts`, so the barrel
|
|
7
|
+
* surface is unchanged; tests may import it directly.
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolGrant } from "./types.ts";
|
|
10
|
+
/**
|
|
11
|
+
* Minimatch-style glob to RegExp: `**` spans separators, `*` does not.
|
|
12
|
+
*
|
|
13
|
+
* `**` followed by `/` is zero or more COMPLETE directory segments, which is
|
|
14
|
+
* why it does not simply compile to `.*`. `.*` both spans separators and
|
|
15
|
+
* matches the empty string, so emitting it and dropping the separator erased
|
|
16
|
+
* the boundary: `src/**\/config.ts` became `^src\/.*config\.ts$`, and a grant
|
|
17
|
+
* for files named `config.ts` also admitted `src/legacyconfig.ts`.
|
|
18
|
+
*
|
|
19
|
+
* That was never a root escape -- containment runs before any pattern matching
|
|
20
|
+
* -- but it made a *scoped* grant wider than its author wrote, which is the one
|
|
21
|
+
* thing a scoped profile exists to prevent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function globToRegExp(pattern: string): RegExp;
|
|
24
|
+
/** A compiled glob, keeping its source so verb names can be told from paths. */
|
|
25
|
+
export interface CompiledPattern {
|
|
26
|
+
readonly source: string;
|
|
27
|
+
readonly re: RegExp;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A deny/ask rule list compiled for one tool.
|
|
31
|
+
*
|
|
32
|
+
* Unlike the allow compiler (last-write-wins per tool, by design),
|
|
33
|
+
* rules for the same tool MERGE here: patterns are concatenated and any
|
|
34
|
+
* `"*"` makes the whole entry unconditional. That is safe -- and correct --
|
|
35
|
+
* because deny/ask are additive; a later rule must never silently drop an
|
|
36
|
+
* earlier one.
|
|
37
|
+
*/
|
|
38
|
+
export interface CompiledEntry {
|
|
39
|
+
readonly unconditional: boolean;
|
|
40
|
+
readonly matchers: CompiledPattern[];
|
|
41
|
+
readonly argvPatterns: readonly (readonly CompiledPattern[])[];
|
|
42
|
+
readonly raw: readonly string[];
|
|
43
|
+
/** Patterns grouped by their original rule expression for ask telemetry. */
|
|
44
|
+
readonly rulePatterns?: readonly (readonly string[])[];
|
|
45
|
+
}
|
|
46
|
+
export declare function matchesAny(patterns: readonly CompiledPattern[], value: string): boolean;
|
|
47
|
+
/** The source of the first compiled glob matching `value`, for naming a rule. */
|
|
48
|
+
export declare function matchedGlobSource(patterns: readonly CompiledPattern[], value: string): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Split a grant pattern into whitespace-separated tokens, each compiled
|
|
51
|
+
* independently. Per-token compilation (rather than joining argv with
|
|
52
|
+
* spaces and matching one regex against the joined string) is deliberate:
|
|
53
|
+
* a joined string lets a value containing spaces or glob metacharacters
|
|
54
|
+
* shift what a later token appears to match. `"bun add*"` must mean "argv[0]
|
|
55
|
+
* is exactly bun, argv[1] starts with add", never "the space-joined argv
|
|
56
|
+
* matches this glob".
|
|
57
|
+
*/
|
|
58
|
+
export declare function compileArgvPattern(pattern: string): readonly CompiledPattern[];
|
|
59
|
+
/** A grant pattern's tokens are a PREFIX of argv — trailing argv tokens
|
|
60
|
+
* (the package name, flags, ...) are the payload a prefix grant like
|
|
61
|
+
* `bun add*` does not itself constrain. */
|
|
62
|
+
export declare function matchesArgvPattern(tokens: readonly CompiledPattern[], argv: readonly string[]): boolean;
|
|
63
|
+
export declare function matchesArgvGrant(argvPatterns: readonly (readonly CompiledPattern[])[], argv: readonly string[]): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Compile a deny/ask list. Deliberately separate from the allow compiler: the
|
|
66
|
+
* allow map overwrites, and unifying the two would change verdicts for any
|
|
67
|
+
* config carrying two expressions for one tool (the byte-identity regression
|
|
68
|
+
* gate pins that). Merging is safe for deny/ask because those lists are new
|
|
69
|
+
* and additive.
|
|
70
|
+
*/
|
|
71
|
+
export declare function compileRuleMap(rules: readonly ToolGrant[] | undefined): Map<string, CompiledEntry>;
|
|
72
|
+
/** The configured expression-group containing a matched pattern, if any. */
|
|
73
|
+
export declare function matchedRulePatterns(entry: CompiledEntry, source: string): readonly string[];
|
|
74
|
+
/** The original pattern whose tokens matched `argv`, for naming an ask rule. */
|
|
75
|
+
export declare function matchedArgvSource(entry: CompiledEntry, argv: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern compilation for the tool policy: globs, argv tokens, and the
|
|
3
|
+
* deny/ask rule maps.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `policy.ts` to keep that file under the 600-line source
|
|
6
|
+
* limit. `src/` reaches this module only through `policy.ts`, so the barrel
|
|
7
|
+
* surface is unchanged; tests may import it directly.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Minimatch-style glob to RegExp: `**` spans separators, `*` does not.
|
|
11
|
+
*
|
|
12
|
+
* `**` followed by `/` is zero or more COMPLETE directory segments, which is
|
|
13
|
+
* why it does not simply compile to `.*`. `.*` both spans separators and
|
|
14
|
+
* matches the empty string, so emitting it and dropping the separator erased
|
|
15
|
+
* the boundary: `src/**\/config.ts` became `^src\/.*config\.ts$`, and a grant
|
|
16
|
+
* for files named `config.ts` also admitted `src/legacyconfig.ts`.
|
|
17
|
+
*
|
|
18
|
+
* That was never a root escape -- containment runs before any pattern matching
|
|
19
|
+
* -- but it made a *scoped* grant wider than its author wrote, which is the one
|
|
20
|
+
* thing a scoped profile exists to prevent.
|
|
21
|
+
*/
|
|
22
|
+
export function globToRegExp(pattern) {
|
|
23
|
+
let out = "";
|
|
24
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
25
|
+
const char = pattern[i];
|
|
26
|
+
if (char === "*") {
|
|
27
|
+
if (pattern[i + 1] === "*") {
|
|
28
|
+
i += 1;
|
|
29
|
+
if (pattern[i + 1] === "/") {
|
|
30
|
+
// Optional, so `x/**/y` still matches `x/y` -- minimatch's behaviour.
|
|
31
|
+
out += "(?:.*/)?";
|
|
32
|
+
i += 1;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
out += ".*";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
out += "[^/]*";
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === "?") {
|
|
44
|
+
out += "[^/]";
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
48
|
+
}
|
|
49
|
+
return new RegExp(`^${out}$`);
|
|
50
|
+
}
|
|
51
|
+
export function matchesAny(patterns, value) {
|
|
52
|
+
return patterns.some((p) => p.re.test(value));
|
|
53
|
+
}
|
|
54
|
+
/** The source of the first compiled glob matching `value`, for naming a rule. */
|
|
55
|
+
export function matchedGlobSource(patterns, value) {
|
|
56
|
+
return patterns.find((p) => p.re.test(value))?.source;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Split a grant pattern into whitespace-separated tokens, each compiled
|
|
60
|
+
* independently. Per-token compilation (rather than joining argv with
|
|
61
|
+
* spaces and matching one regex against the joined string) is deliberate:
|
|
62
|
+
* a joined string lets a value containing spaces or glob metacharacters
|
|
63
|
+
* shift what a later token appears to match. `"bun add*"` must mean "argv[0]
|
|
64
|
+
* is exactly bun, argv[1] starts with add", never "the space-joined argv
|
|
65
|
+
* matches this glob".
|
|
66
|
+
*/
|
|
67
|
+
export function compileArgvPattern(pattern) {
|
|
68
|
+
return pattern
|
|
69
|
+
.split(/\s+/)
|
|
70
|
+
.filter((token) => token.length > 0)
|
|
71
|
+
.map((token) => ({ source: token, re: globToRegExp(token) }));
|
|
72
|
+
}
|
|
73
|
+
/** A grant pattern's tokens are a PREFIX of argv — trailing argv tokens
|
|
74
|
+
* (the package name, flags, ...) are the payload a prefix grant like
|
|
75
|
+
* `bun add*` does not itself constrain. */
|
|
76
|
+
export function matchesArgvPattern(tokens, argv) {
|
|
77
|
+
if (tokens.length > argv.length)
|
|
78
|
+
return false;
|
|
79
|
+
return tokens.every((token, i) => token.re.test(argv[i]));
|
|
80
|
+
}
|
|
81
|
+
export function matchesArgvGrant(argvPatterns, argv) {
|
|
82
|
+
return argvPatterns.some((tokens) => matchesArgvPattern(tokens, argv));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compile a deny/ask list. Deliberately separate from the allow compiler: the
|
|
86
|
+
* allow map overwrites, and unifying the two would change verdicts for any
|
|
87
|
+
* config carrying two expressions for one tool (the byte-identity regression
|
|
88
|
+
* gate pins that). Merging is safe for deny/ask because those lists are new
|
|
89
|
+
* and additive.
|
|
90
|
+
*/
|
|
91
|
+
export function compileRuleMap(rules) {
|
|
92
|
+
const map = new Map();
|
|
93
|
+
for (const rule of rules ?? []) {
|
|
94
|
+
const existing = map.get(rule.tool);
|
|
95
|
+
const patterns = existing === undefined ? rule.patterns : [...existing.raw, ...rule.patterns];
|
|
96
|
+
const nonWildcard = patterns.filter((p) => p !== "*");
|
|
97
|
+
map.set(rule.tool, {
|
|
98
|
+
unconditional: patterns.includes("*"),
|
|
99
|
+
matchers: nonWildcard.map((source) => ({ source, re: globToRegExp(source) })),
|
|
100
|
+
argvPatterns: nonWildcard.map((source) => compileArgvPattern(source)),
|
|
101
|
+
raw: patterns,
|
|
102
|
+
rulePatterns: [...(existing?.rulePatterns ?? []), rule.patterns],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return map;
|
|
106
|
+
}
|
|
107
|
+
/** The configured expression-group containing a matched pattern, if any. */
|
|
108
|
+
export function matchedRulePatterns(entry, source) {
|
|
109
|
+
return entry.rulePatterns?.find((patterns) => patterns.includes(source)) ?? entry.raw;
|
|
110
|
+
}
|
|
111
|
+
/** The original pattern whose tokens matched `argv`, for naming an ask rule. */
|
|
112
|
+
export function matchedArgvSource(entry, argv) {
|
|
113
|
+
const index = entry.argvPatterns.findIndex((tokens) => matchesArgvPattern(tokens, argv));
|
|
114
|
+
if (index === -1)
|
|
115
|
+
return undefined;
|
|
116
|
+
return entry.raw.filter((p) => p !== "*")[index];
|
|
117
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy's path branch: containment first, then deny -> ask -> allow per
|
|
3
|
+
* resolved path, across the four kinds of path-bearing input field.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `policy.ts` (cognitive-complexity drain A8). Each field kind is
|
|
6
|
+
* its own named handler, and the per-path sequence all four share — resolve,
|
|
7
|
+
* refuse containment breaches, apply rules, glob-check, record — is one helper
|
|
8
|
+
* whose arguments name the only things that differ between the loops: the
|
|
9
|
+
* denial's subject spelling, and whether a glob miss denies.
|
|
10
|
+
*
|
|
11
|
+
* Behavioural shape worth naming because the handlers must not be "unified":
|
|
12
|
+
* `pathFields` and `listPathFields` deny on a glob miss whenever the grant is
|
|
13
|
+
* conditional (`enforcePathGlobs`), while `arrayPathFields` and `refPathFields`
|
|
14
|
+
* deny only when the grant is conditional AND carries at least one path glob
|
|
15
|
+
* (`restrictPaths`). A verb-only grant therefore DENIES string path fields but
|
|
16
|
+
* leaves array/ref paths bounded by the root alone — pinned by `policy.test.ts`
|
|
17
|
+
* ("a verb-only grant leaves paths bounded by the root alone") and
|
|
18
|
+
* `policy-paths-branch-edges.test.ts`.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is reached through `policy.ts`; this module imports nothing
|
|
21
|
+
* from it (the collaborators it shares — deny, askVerdict, applyPathRules —
|
|
22
|
+
* arrive by reference in `PathsBranchContext`, read at call time).
|
|
23
|
+
*/
|
|
24
|
+
import { type CompiledEntry } from "./policy-match.ts";
|
|
25
|
+
import type { PolicyVerdict, ToolScope } from "./types.ts";
|
|
26
|
+
/** Mutable scratch shared by `check()`'s branch helpers: first ask rule matched. */
|
|
27
|
+
export interface RuleState {
|
|
28
|
+
ask?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Per-policy collaborators the path branch shares with the rest of the policy.
|
|
32
|
+
* Passed by reference; every member is read at call time, so a caller that
|
|
33
|
+
* rebuilds or reassigns between checks stays authoritative.
|
|
34
|
+
*/
|
|
35
|
+
export interface PathsBranchContext {
|
|
36
|
+
/** The policy's containment root, symlink-resolved at compile time. */
|
|
37
|
+
readonly resolvedRoot: string;
|
|
38
|
+
readonly deny: (reason: string, breach?: boolean, escalatable?: boolean) => PolicyVerdict;
|
|
39
|
+
readonly askVerdict: (resolvedPaths: readonly string[], rule: string) => PolicyVerdict;
|
|
40
|
+
readonly applyPathRules: (tool: string, rel: string, state: RuleState) => PolicyVerdict | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Absolute, symlink-resolved form of `candidate` if it lies inside `root`,
|
|
44
|
+
* is not itself (and does not lie under) `.git/`, and is not one of nax's own
|
|
45
|
+
* config files.
|
|
46
|
+
*
|
|
47
|
+
* The single containment seam. Multi-root support (a future configurable
|
|
48
|
+
* extension) changes this function and nothing else, which is why every tool
|
|
49
|
+
* receives an already-resolved path rather than resolving one itself. The
|
|
50
|
+
* `.git/` exclusion lives here for the same reason: it applies to every
|
|
51
|
+
* path-bearing tool uniformly, reads included, rather than being
|
|
52
|
+
* re-remembered per tool -- which is exactly the failure mode a declaration
|
|
53
|
+
* on `ToolScope` would reintroduce (nax#1943). `.git/` sits INSIDE the root,
|
|
54
|
+
* so containment alone never bars it, and an unconditional ("*") grant --
|
|
55
|
+
* what every non-Exec tool gets under the default `unrestricted` profile --
|
|
56
|
+
* skips glob matching entirely, so nothing downstream of this function would
|
|
57
|
+
* catch it either.
|
|
58
|
+
*
|
|
59
|
+
* Pre-single-frame, this seam carried one exception: an `execTouchedPaths` set
|
|
60
|
+
* (Task 10) admitted a workspace install's repo-ROOT manifest/lockfile even
|
|
61
|
+
* though it sat outside the containment root, which was then the story's
|
|
62
|
+
* package dir. The root move made the containment root the repo root, so the
|
|
63
|
+
* manifest is in-root by construction and that carve-out was retired
|
|
64
|
+
* (PR2/Task 13). There is no exception to this seam now.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveWithin(root: string, candidate: string): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Containment runs before any pattern matching and wins over everything.
|
|
69
|
+
* Each resolved path is then evaluated deny -> ask -> allow. A verb-only
|
|
70
|
+
* grant declares no path globs, leaving the root as the only bound --
|
|
71
|
+
* unchanged behaviour, now an authoring choice rather than something the
|
|
72
|
+
* grant syntax could not express.
|
|
73
|
+
*
|
|
74
|
+
* `confineTo` (tool-declared, see `ToolScope`) shifts the root passed to
|
|
75
|
+
* `resolveWithin` from `<root>` to `<root>/<confineTo>`: containment stays
|
|
76
|
+
* the one seam, and only its ROOT changes. `relativeTo` keeps rooting at
|
|
77
|
+
* `resolvedRoot` so grant globs, deny rules and `naxOwnedWriteRefusal`
|
|
78
|
+
* continue to see the canonical repo-root-relative spelling -- authors
|
|
79
|
+
* write `.nax/scratchpad/**`, never `**`, regardless of `confineTo`.
|
|
80
|
+
*
|
|
81
|
+
* `confineTo` is bound to stay INSIDE `resolvedRoot`: an authoring typo of
|
|
82
|
+
* `..` or `../shared` would otherwise widen the containment root past the
|
|
83
|
+
* policy boundary and re-scope `resolveWithin`'s `.git/`-metadata and
|
|
84
|
+
* `isNaxConfigFile` protections to a root that no longer aligns with the
|
|
85
|
+
* segments those checks assume -- the repo's own `.nax/config.json` would
|
|
86
|
+
stop being segment-matched against `.nax`. The boundary is the policy
|
|
87
|
+
root's invariant, so an out-of-root confineTo refuses the call outright
|
|
88
|
+
rather than silently widening.
|
|
89
|
+
*/
|
|
90
|
+
export declare function pathsBranch(args: {
|
|
91
|
+
readonly ctx: PathsBranchContext;
|
|
92
|
+
readonly tool: string;
|
|
93
|
+
readonly scope: ToolScope;
|
|
94
|
+
readonly input: Record<string, unknown>;
|
|
95
|
+
readonly grant: CompiledEntry;
|
|
96
|
+
readonly state: RuleState;
|
|
97
|
+
}): PolicyVerdict;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy's path branch: containment first, then deny -> ask -> allow per
|
|
3
|
+
* resolved path, across the four kinds of path-bearing input field.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `policy.ts` (cognitive-complexity drain A8). Each field kind is
|
|
6
|
+
* its own named handler, and the per-path sequence all four share — resolve,
|
|
7
|
+
* refuse containment breaches, apply rules, glob-check, record — is one helper
|
|
8
|
+
* whose arguments name the only things that differ between the loops: the
|
|
9
|
+
* denial's subject spelling, and whether a glob miss denies.
|
|
10
|
+
*
|
|
11
|
+
* Behavioural shape worth naming because the handlers must not be "unified":
|
|
12
|
+
* `pathFields` and `listPathFields` deny on a glob miss whenever the grant is
|
|
13
|
+
* conditional (`enforcePathGlobs`), while `arrayPathFields` and `refPathFields`
|
|
14
|
+
* deny only when the grant is conditional AND carries at least one path glob
|
|
15
|
+
* (`restrictPaths`). A verb-only grant therefore DENIES string path fields but
|
|
16
|
+
* leaves array/ref paths bounded by the root alone — pinned by `policy.test.ts`
|
|
17
|
+
* ("a verb-only grant leaves paths bounded by the root alone") and
|
|
18
|
+
* `policy-paths-branch-edges.test.ts`.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is reached through `policy.ts`; this module imports nothing
|
|
21
|
+
* from it (the collaborators it shares — deny, askVerdict, applyPathRules —
|
|
22
|
+
* arrive by reference in `PathsBranchContext`, read at call time).
|
|
23
|
+
*/
|
|
24
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
25
|
+
import { isInside, realOrRaw } from "#src/internal/realpath";
|
|
26
|
+
import { isNaxConfigFile } from "./nax-owned-writes.js";
|
|
27
|
+
import { pathListElements } from "./path-list.js";
|
|
28
|
+
import { pathFieldValue } from "./policy-input.js";
|
|
29
|
+
import { matchesAny } from "./policy-match.js";
|
|
30
|
+
/**
|
|
31
|
+
* Does `resolved` (already absolute and symlink-resolved) enter a `.git`
|
|
32
|
+
* directory anywhere along its path relative to `root`?
|
|
33
|
+
*
|
|
34
|
+
* A path-SEGMENT match, never a prefix or substring one: `.gitignore` and
|
|
35
|
+
* `.github/` are ordinary tracked names a tool must still be able to reach,
|
|
36
|
+
* and `startsWith(".git")` would wrongly swallow both -- the exact defect
|
|
37
|
+
* this function exists to avoid (nax#1943).
|
|
38
|
+
*
|
|
39
|
+
* Matches ANY segment, not only a leading one. A nested repository or a
|
|
40
|
+
* submodule checked into the tree (`vendor/some-lib/.git`, which may be a
|
|
41
|
+
* directory or, for a submodule, a file pointing at the real gitdir
|
|
42
|
+
* elsewhere) carries the same integrity and containment risk `.git/` at the
|
|
43
|
+
* root does -- the ruling behind this function is "no path-bearing tool ever
|
|
44
|
+
* addresses git metadata", not "only the top-level repository's".
|
|
45
|
+
*/
|
|
46
|
+
function entersGitMetadata(root, resolved) {
|
|
47
|
+
const rel = relative(realOrRaw(root), resolved);
|
|
48
|
+
if (rel === "" || rel.startsWith(".."))
|
|
49
|
+
return false;
|
|
50
|
+
return rel.split(sep).includes(".git");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Absolute, symlink-resolved form of `candidate` if it lies inside `root`,
|
|
54
|
+
* is not itself (and does not lie under) `.git/`, and is not one of nax's own
|
|
55
|
+
* config files.
|
|
56
|
+
*
|
|
57
|
+
* The single containment seam. Multi-root support (a future configurable
|
|
58
|
+
* extension) changes this function and nothing else, which is why every tool
|
|
59
|
+
* receives an already-resolved path rather than resolving one itself. The
|
|
60
|
+
* `.git/` exclusion lives here for the same reason: it applies to every
|
|
61
|
+
* path-bearing tool uniformly, reads included, rather than being
|
|
62
|
+
* re-remembered per tool -- which is exactly the failure mode a declaration
|
|
63
|
+
* on `ToolScope` would reintroduce (nax#1943). `.git/` sits INSIDE the root,
|
|
64
|
+
* so containment alone never bars it, and an unconditional ("*") grant --
|
|
65
|
+
* what every non-Exec tool gets under the default `unrestricted` profile --
|
|
66
|
+
* skips glob matching entirely, so nothing downstream of this function would
|
|
67
|
+
* catch it either.
|
|
68
|
+
*
|
|
69
|
+
* Pre-single-frame, this seam carried one exception: an `execTouchedPaths` set
|
|
70
|
+
* (Task 10) admitted a workspace install's repo-ROOT manifest/lockfile even
|
|
71
|
+
* though it sat outside the containment root, which was then the story's
|
|
72
|
+
* package dir. The root move made the containment root the repo root, so the
|
|
73
|
+
* manifest is in-root by construction and that carve-out was retired
|
|
74
|
+
* (PR2/Task 13). There is no exception to this seam now.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveWithin(root, candidate) {
|
|
77
|
+
const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
|
|
78
|
+
if (isInside(root, absolute)) {
|
|
79
|
+
const resolved = realOrRaw(absolute);
|
|
80
|
+
if (entersGitMetadata(root, resolved) || isNaxConfigFile(root, resolved))
|
|
81
|
+
return null;
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The reason text for a path `resolveWithin` refused (fix round 1, Task 10;
|
|
88
|
+
* `.git/` case added for nax#1943).
|
|
89
|
+
*
|
|
90
|
+
* A bare "resolves outside the permitted root" taught the model nothing
|
|
91
|
+
* the last time this shape of denial mattered: in the run that motivated
|
|
92
|
+
* this whole feature, that message is what led an agent to delete a
|
|
93
|
+
* tsconfig entry instead of installing the package it needed. The design's
|
|
94
|
+
* own rule is that a denial returns the reason AND what would have been
|
|
95
|
+
* allowed.
|
|
96
|
+
*
|
|
97
|
+
* A `.git/`-metadata refusal gets its own branch, checked first: unlike
|
|
98
|
+
* every other refusal this function handles, the candidate is genuinely
|
|
99
|
+
* INSIDE the root, so "resolves outside the permitted root" would be
|
|
100
|
+
* actively misleading rather than merely unhelpful.
|
|
101
|
+
*
|
|
102
|
+
* Every other refused path keeps the plain message. The GitCommit-specific
|
|
103
|
+
* manifest/lockfile message and its `isKnownManifestOrLockfileName` table
|
|
104
|
+
* were retired with the `execTouchedPaths` carve-out (PR2/Task 13): that
|
|
105
|
+
* message existed only to explain the carve-out's rule, and post root move
|
|
106
|
+
* a repo-root manifest is inside the root by construction, so there is no
|
|
107
|
+
* distinct rule left to explain.
|
|
108
|
+
*
|
|
109
|
+
* This must never get chattier for ordinary containment denials, and must
|
|
110
|
+
* never reveal repository structure for a path the model never touched.
|
|
111
|
+
*
|
|
112
|
+
* The root itself IS named, deliberately. The rule above -- never reveal
|
|
113
|
+
* repository structure -- is about paths the model never touched; this path is
|
|
114
|
+
* one the model just passed, and telling it where the boundary is is the
|
|
115
|
+
* difference between "adapt" and "work around". The dispatch preamble
|
|
116
|
+
* (src/prompts/sections/agent-scope.ts) states the same boundary only as a
|
|
117
|
+
* package-relative label (`packages/api`), never as an absolute path, so this
|
|
118
|
+
* message is where the agent first sees the absolute containment root -- and
|
|
119
|
+
* naming a path the model itself handed in is the right disclosure.
|
|
120
|
+
*/
|
|
121
|
+
function outOfRootReason(root, candidate) {
|
|
122
|
+
const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
|
|
123
|
+
if (isInside(root, absolute) && isNaxConfigFile(root, realOrRaw(absolute))) {
|
|
124
|
+
return ("is one of nax's own config files, which every tool is refused regardless of grant -- " +
|
|
125
|
+
"`quality.commands` and `acceptance.command` are run through a shell WITHOUT passing the " +
|
|
126
|
+
"permission gate because a human wrote them, so editing this file is a route to running " +
|
|
127
|
+
"an ungated command on the next run");
|
|
128
|
+
}
|
|
129
|
+
if (isInside(root, absolute) && entersGitMetadata(root, realOrRaw(absolute))) {
|
|
130
|
+
return ("targets git metadata under .git/, which every tool is refused regardless of grant -- " +
|
|
131
|
+
"writing there can corrupt the repository beyond git's own recovery, and reading its " +
|
|
132
|
+
"config is a route to influencing what nax executes without passing through Exec (nax#1943)");
|
|
133
|
+
}
|
|
134
|
+
return `resolves outside the permitted root (${root}), which is the only directory this tool can reach`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The path globs in a grant, which for a verb-gated tool means the patterns
|
|
138
|
+
* that are not verb names.
|
|
139
|
+
*
|
|
140
|
+
* A grant list is overloaded -- verbs for Git, path globs for Write -- so
|
|
141
|
+
* matching every pattern against a path denied everything for Git, and
|
|
142
|
+
* matching none left its paths bounded by the root alone. `allowedVerbs` is a
|
|
143
|
+
* closed set the tool declares, so the two kinds separate without guessing.
|
|
144
|
+
*/
|
|
145
|
+
function pathMatchers(matchers, scope) {
|
|
146
|
+
const verbs = scope.allowedVerbs;
|
|
147
|
+
return verbs === undefined ? matchers : matchers.filter((m) => !verbs.includes(m.source));
|
|
148
|
+
}
|
|
149
|
+
/** Canonical repo-root-relative spelling grant globs and rules are matched against. */
|
|
150
|
+
function repoRelative(resolvedRoot, resolved) {
|
|
151
|
+
return relative(resolvedRoot, resolved).split(sep).join("/");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A path spelled with the confined prefix the tool is already bound to
|
|
155
|
+
* (`.nax/scratchpad/spill/x.txt`) has it removed before `resolveWithin`
|
|
156
|
+
* runs, so both spellings reach the same file. This is a prefix REMOVAL,
|
|
157
|
+
* not a second containment root: what is left is still resolved inside
|
|
158
|
+
* the effective root, so a `..` after the prefix remains a breach.
|
|
159
|
+
*/
|
|
160
|
+
function stripConfinePrefix(confinePrefix, value) {
|
|
161
|
+
return confinePrefix !== "" && value.startsWith(confinePrefix) ? value.slice(confinePrefix.length) : value;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolve one candidate inside the effective root, then evaluate
|
|
165
|
+
* deny -> ask -> glob -> record. Returns a verdict to stop the walk with, or
|
|
166
|
+
* undefined when the candidate is admitted and the walk continues.
|
|
167
|
+
* `enforceGlobs` is the CALLER's glob predicate (`enforcePathGlobs` for string
|
|
168
|
+
* fields, `restrictPaths` for array/ref fields — see the module comment).
|
|
169
|
+
*/
|
|
170
|
+
function checkPathSegment(frame, candidate, enforceGlobs) {
|
|
171
|
+
const resolved = resolveWithin(frame.effectiveRoot, stripConfinePrefix(frame.confinePrefix, candidate.path));
|
|
172
|
+
if (resolved === null) {
|
|
173
|
+
return frame.ctx.deny(`${candidate.denialSubject} ${outOfRootReason(frame.effectiveRoot, candidate.outOfRootTarget)}`, true);
|
|
174
|
+
}
|
|
175
|
+
const rel = repoRelative(frame.ctx.resolvedRoot, resolved);
|
|
176
|
+
const ruleDenial = frame.ctx.applyPathRules(frame.tool, rel, frame.state);
|
|
177
|
+
if (ruleDenial !== undefined)
|
|
178
|
+
return ruleDenial;
|
|
179
|
+
if (enforceGlobs && !matchesAny(frame.globs, rel)) {
|
|
180
|
+
return frame.ctx.deny(`${frame.tool} is not granted "${rel}" for this stage`);
|
|
181
|
+
}
|
|
182
|
+
frame.resolvedPaths.push(resolved);
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
/** `scope.pathFields`: each field holds ONE path, checked as a whole. */
|
|
186
|
+
function runPathFields(frame) {
|
|
187
|
+
for (const field of frame.scope.pathFields) {
|
|
188
|
+
const value = pathFieldValue(frame.input, field);
|
|
189
|
+
if (value === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
if (typeof value !== "string")
|
|
192
|
+
return frame.ctx.deny(`"${field}" must be a string path`);
|
|
193
|
+
const denial = checkPathSegment(frame, { path: value, denialSubject: `path "${value}"`, outOfRootTarget: value }, frame.enforcePathGlobs);
|
|
194
|
+
if (denial !== undefined)
|
|
195
|
+
return denial;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
/** `scope.listPathFields`: each field holds a whitespace-separated list, or an array, of paths. */
|
|
200
|
+
function runListPathFields(frame) {
|
|
201
|
+
for (const field of frame.scope.listPathFields ?? []) {
|
|
202
|
+
const value = pathFieldValue(frame.input, field);
|
|
203
|
+
if (value === undefined)
|
|
204
|
+
continue;
|
|
205
|
+
const elements = typeof value === "string"
|
|
206
|
+
? pathListElements(value, frame.effectiveRoot)
|
|
207
|
+
: Array.isArray(value) && value.every((element) => typeof element === "string")
|
|
208
|
+
? value
|
|
209
|
+
: null;
|
|
210
|
+
if (elements === null)
|
|
211
|
+
return frame.ctx.deny(`"${field}" must be a string path or an array of string paths`);
|
|
212
|
+
for (const element of elements) {
|
|
213
|
+
const denial = checkPathSegment(frame, { path: element, denialSubject: `path "${element}"`, outOfRootTarget: element }, frame.enforcePathGlobs);
|
|
214
|
+
if (denial !== undefined)
|
|
215
|
+
return denial;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
/** `scope.arrayPathFields`: each field holds an array whose every element is a path. */
|
|
221
|
+
function runArrayPathFields(frame) {
|
|
222
|
+
for (const field of frame.scope.arrayPathFields ?? []) {
|
|
223
|
+
const values = frame.input[field];
|
|
224
|
+
if (values === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
if (!Array.isArray(values))
|
|
227
|
+
return frame.ctx.deny(`"${field}" must be an array of string paths`);
|
|
228
|
+
for (const value of values) {
|
|
229
|
+
if (typeof value !== "string")
|
|
230
|
+
return frame.ctx.deny(`"${field}" entries must be strings`);
|
|
231
|
+
const denial = checkPathSegment(frame, { path: value, denialSubject: `"${field}" entry "${value}"`, outOfRootTarget: value }, frame.restrictPaths);
|
|
232
|
+
if (denial !== undefined)
|
|
233
|
+
return denial;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
/** `scope.refPathFields`: each field holds an array of refs that MAY carry a `<rev>:<path>` path half. */
|
|
239
|
+
function runRefPathFields(frame) {
|
|
240
|
+
for (const field of frame.scope.refPathFields ?? []) {
|
|
241
|
+
const values = frame.input[field];
|
|
242
|
+
if (values === undefined)
|
|
243
|
+
continue;
|
|
244
|
+
if (!Array.isArray(values))
|
|
245
|
+
return frame.ctx.deny(`"${field}" must be an array of string refs`);
|
|
246
|
+
for (const value of values) {
|
|
247
|
+
if (typeof value !== "string")
|
|
248
|
+
return frame.ctx.deny(`"${field}" entries must be strings`);
|
|
249
|
+
const colonAt = value.indexOf(":");
|
|
250
|
+
if (colonAt === -1)
|
|
251
|
+
continue; // pure revision, no path to check
|
|
252
|
+
const candidatePath = value.slice(colonAt + 1);
|
|
253
|
+
if (candidatePath === "")
|
|
254
|
+
continue; // e.g. "HEAD:" — no path to check
|
|
255
|
+
const denial = checkPathSegment(frame, { path: candidatePath, denialSubject: `"${field}" entry "${value}"`, outOfRootTarget: candidatePath }, frame.restrictPaths);
|
|
256
|
+
if (denial !== undefined)
|
|
257
|
+
return denial;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Containment runs before any pattern matching and wins over everything.
|
|
264
|
+
* Each resolved path is then evaluated deny -> ask -> allow. A verb-only
|
|
265
|
+
* grant declares no path globs, leaving the root as the only bound --
|
|
266
|
+
* unchanged behaviour, now an authoring choice rather than something the
|
|
267
|
+
* grant syntax could not express.
|
|
268
|
+
*
|
|
269
|
+
* `confineTo` (tool-declared, see `ToolScope`) shifts the root passed to
|
|
270
|
+
* `resolveWithin` from `<root>` to `<root>/<confineTo>`: containment stays
|
|
271
|
+
* the one seam, and only its ROOT changes. `relativeTo` keeps rooting at
|
|
272
|
+
* `resolvedRoot` so grant globs, deny rules and `naxOwnedWriteRefusal`
|
|
273
|
+
* continue to see the canonical repo-root-relative spelling -- authors
|
|
274
|
+
* write `.nax/scratchpad/**`, never `**`, regardless of `confineTo`.
|
|
275
|
+
*
|
|
276
|
+
* `confineTo` is bound to stay INSIDE `resolvedRoot`: an authoring typo of
|
|
277
|
+
* `..` or `../shared` would otherwise widen the containment root past the
|
|
278
|
+
* policy boundary and re-scope `resolveWithin`'s `.git/`-metadata and
|
|
279
|
+
* `isNaxConfigFile` protections to a root that no longer aligns with the
|
|
280
|
+
* segments those checks assume -- the repo's own `.nax/config.json` would
|
|
281
|
+
stop being segment-matched against `.nax`. The boundary is the policy
|
|
282
|
+
root's invariant, so an out-of-root confineTo refuses the call outright
|
|
283
|
+
rather than silently widening.
|
|
284
|
+
*/
|
|
285
|
+
export function pathsBranch(args) {
|
|
286
|
+
const { ctx, tool, scope, input, grant, state } = args;
|
|
287
|
+
const globs = pathMatchers(grant.matchers, scope);
|
|
288
|
+
let effectiveRoot = ctx.resolvedRoot;
|
|
289
|
+
if (scope.confineTo !== undefined) {
|
|
290
|
+
effectiveRoot = realOrRaw(join(ctx.resolvedRoot, scope.confineTo));
|
|
291
|
+
if (!isInside(ctx.resolvedRoot, effectiveRoot)) {
|
|
292
|
+
return ctx.deny(`${tool} declares confineTo "${scope.confineTo}" which resolves outside the policy root "${ctx.resolvedRoot}" -- confineTo must be a path INSIDE the policy root, never one that widens it`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const frame = {
|
|
296
|
+
ctx,
|
|
297
|
+
tool,
|
|
298
|
+
scope,
|
|
299
|
+
input,
|
|
300
|
+
globs,
|
|
301
|
+
effectiveRoot,
|
|
302
|
+
state,
|
|
303
|
+
resolvedPaths: [],
|
|
304
|
+
confinePrefix: scope.confineTo === undefined ? "" : `${scope.confineTo}/`,
|
|
305
|
+
enforcePathGlobs: !grant.unconditional,
|
|
306
|
+
restrictPaths: !grant.unconditional && globs.length > 0,
|
|
307
|
+
};
|
|
308
|
+
const denial = runPathFields(frame) ?? runListPathFields(frame) ?? runArrayPathFields(frame) ?? runRefPathFields(frame);
|
|
309
|
+
if (denial !== undefined)
|
|
310
|
+
return denial;
|
|
311
|
+
return state.ask === undefined
|
|
312
|
+
? { allowed: true, resolvedPaths: frame.resolvedPaths }
|
|
313
|
+
: ctx.askVerdict(frame.resolvedPaths, state.ask);
|
|
314
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile declarative grants into a policy, and answer one call at a time.
|
|
3
|
+
*
|
|
4
|
+
* nax-permission-mode-allow: applies grants resolved by resolvePermissions;
|
|
5
|
+
* decides no permission of its own.
|
|
6
|
+
*
|
|
7
|
+
* Containment runs BEFORE pattern matching and is not expressible in config:
|
|
8
|
+
* the root is a boundary no PROFILE can widen. `unrestricted` means "any
|
|
9
|
+
* tool, any path within the root", never "any path on the machine".
|
|
10
|
+
*
|
|
11
|
+
* The `execTouchedPaths` carve-out (Task 10) was retired in the single-frame
|
|
12
|
+
* root move (PR2/Task 13): the containment root became the repo root, so a
|
|
13
|
+
* workspace install's root manifest is in-root by construction and no
|
|
14
|
+
* exception to the boundary is needed.
|
|
15
|
+
*/
|
|
16
|
+
import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
17
|
+
import { resolveWithin } from "./policy-paths-branch.ts";
|
|
18
|
+
import { type ToolGrant, type ToolPolicy } from "./types.ts";
|
|
19
|
+
/** The single containment seam now lives beside the path branch that drives it. */
|
|
20
|
+
export { resolveWithin };
|
|
21
|
+
export interface ToolPolicyOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Stage deny rules (spec R6). Same {tool, patterns} shape as grants; matched
|
|
24
|
+
* per branch and evaluated BEFORE ask and allow. A tool's unconditional
|
|
25
|
+
* deny rule also de-advertises it (see `grantedTools`).
|
|
26
|
+
*/
|
|
27
|
+
readonly denyRules?: readonly ToolGrant[];
|
|
28
|
+
/**
|
|
29
|
+
* Stage ask rules (spec R1/R6). An ask match does not grant or refuse on its
|
|
30
|
+
* own: it marks a call the stage already granted as needing approval. An ask
|
|
31
|
+
* on an ungranted call is a plain denial.
|
|
32
|
+
*/
|
|
33
|
+
readonly askRules?: readonly ToolGrant[];
|
|
34
|
+
/**
|
|
35
|
+
* The ONE nax-owned path this session may write despite `naxOwnedWriteRefusal`
|
|
36
|
+
* (nax#2115): the ABSOLUTE `fileOutput` path the dispatching op declared. It is
|
|
37
|
+
* canonicalised into this policy's own root-relative frame below rather than by
|
|
38
|
+
* the caller, so alternate spellings of the same file cannot diverge from the
|
|
39
|
+
* string the guard compares. Only the feature-PRD refusal honours it; the nax
|
|
40
|
+
* CONFIG refusal is deliberately not exempted.
|
|
41
|
+
*/
|
|
42
|
+
readonly ownedWriteExemption?: string;
|
|
43
|
+
/** nax#2260: `execution.sandbox.filesystem.allowWrite`; a listed top-level `.nax/` entry becomes writable. */
|
|
44
|
+
readonly naxAllowWrite?: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* How a bash command string is adjudicated (ADR-030). Absent means `gated` —
|
|
47
|
+
* today's behaviour — so every caller that does not opt in is unchanged.
|
|
48
|
+
*
|
|
49
|
+
* `raw` is a COMPILE-TIME input rather than a post-check transform on
|
|
50
|
+
* purpose: `checkBashCommand` lexes before it evaluates grants, so a `Bash(*)`
|
|
51
|
+
* grant cannot produce pass-through, and a post-check deny→allow would widen
|
|
52
|
+
* genuine containment denials too.
|
|
53
|
+
*/
|
|
54
|
+
readonly bashApproval?: BashApprovalMode;
|
|
55
|
+
/**
|
|
56
|
+
* P4: set when `execution.sandbox.enabled` is true and the probe found the
|
|
57
|
+
* sandbox unavailable. Under `raw`, every Bash call is then denied with this
|
|
58
|
+
* reason -- raw requires the sandbox once it is enabled, and a silent
|
|
59
|
+
* fallback to unsandboxed raw would be a posture downgrade (spec S5).
|
|
60
|
+
* Ignored under gated/escalate, which run unwrapped with a warning.
|
|
61
|
+
*/
|
|
62
|
+
readonly rawBashRefusal?: string;
|
|
63
|
+
readonly sandboxWrapped?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function compileToolPolicy(grants: readonly ToolGrant[], root: string, options?: ToolPolicyOptions): ToolPolicy;
|