@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose links into a resolver. First non-abstain wins.
|
|
3
|
+
*
|
|
4
|
+
* A throwing link abstains rather than propagating: `runtime.callTool` turns an
|
|
5
|
+
* exception out of the resolver into a TOOL ERROR surfaced to the model, not a
|
|
6
|
+
* denial, which would lose the `denied:ask` ledger row and hand the agent
|
|
7
|
+
* something it may retry around.
|
|
8
|
+
*
|
|
9
|
+
* US-003: forwards the same `control` object to every link so a turn-cancel
|
|
10
|
+
* signal reaches whichever link would have answered, not just the first one
|
|
11
|
+
* that asks.
|
|
12
|
+
*/
|
|
13
|
+
export function chainAskLinks(links) {
|
|
14
|
+
return {
|
|
15
|
+
async resolve(req, control) {
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
for (const link of links) {
|
|
18
|
+
let outcome;
|
|
19
|
+
try {
|
|
20
|
+
outcome = await link.resolve(req, control);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (outcome.decision === "abstain")
|
|
26
|
+
continue;
|
|
27
|
+
return { decision: outcome.decision, decidedBy: outcome.decidedBy, latencyMs: Date.now() - started };
|
|
28
|
+
}
|
|
29
|
+
return { decision: "deny", decidedBy: "unavailable", latencyMs: Date.now() - started };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AskResolver } from "./ask-chain.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Why an ask-matched call was refused. One string per CASE: the previous single
|
|
4
|
+
* message asserted "this run is headless" for situations that are not, and the
|
|
5
|
+
* three are materially different facts for anyone reading the ledger.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ASK_NO_CHANNEL_REASON = "matched an ask rule requiring human approval; no approval channel is configured for this run, so the call is refused";
|
|
8
|
+
export declare const ASK_TIMEOUT_REASON = "matched an ask rule requiring human approval; no answer arrived before the approval timeout, so the call is refused";
|
|
9
|
+
export declare const ASK_DENIED_REASON = "matched an ask rule requiring human approval; the operator denied it";
|
|
10
|
+
/**
|
|
11
|
+
* US-003: the orchestrating turn was cancelled before anyone answered (or
|
|
12
|
+
* between the resolver returning allow and the tool actually running). The
|
|
13
|
+
* tool never executed; the message stays short so it reads in a tool error.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ASK_CANCELLED_REASON = "Not run: the turn was cancelled before anyone answered.";
|
|
16
|
+
export declare const ASK_UNSHOWABLE_REASON = "Not run: the command contains a secret that cannot be shown to the approver safely; pass it through an environment variable instead.";
|
|
17
|
+
/** Kept for compatibility with existing callers and tests. */
|
|
18
|
+
export declare const ASK_UNAVAILABLE_REASON = "matched an ask rule requiring human approval; no approval channel is configured for this run, so the call is refused";
|
|
19
|
+
/**
|
|
20
|
+
* The resolver a run gets when no channel is available: a chain with zero
|
|
21
|
+
* links, whose terminal deny supplies the answer. TOTAL -- never abstains.
|
|
22
|
+
*/
|
|
23
|
+
export declare function headlessAskResolver(): AskResolver;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { chainAskLinks } from "./ask-chain.js";
|
|
2
|
+
/**
|
|
3
|
+
* Why an ask-matched call was refused. One string per CASE: the previous single
|
|
4
|
+
* message asserted "this run is headless" for situations that are not, and the
|
|
5
|
+
* three are materially different facts for anyone reading the ledger.
|
|
6
|
+
*/
|
|
7
|
+
export const ASK_NO_CHANNEL_REASON = "matched an ask rule requiring human approval; no approval channel is configured for this run, so the call is refused";
|
|
8
|
+
export const ASK_TIMEOUT_REASON = "matched an ask rule requiring human approval; no answer arrived before the approval timeout, so the call is refused";
|
|
9
|
+
export const ASK_DENIED_REASON = "matched an ask rule requiring human approval; the operator denied it";
|
|
10
|
+
/**
|
|
11
|
+
* US-003: the orchestrating turn was cancelled before anyone answered (or
|
|
12
|
+
* between the resolver returning allow and the tool actually running). The
|
|
13
|
+
* tool never executed; the message stays short so it reads in a tool error.
|
|
14
|
+
*/
|
|
15
|
+
export const ASK_CANCELLED_REASON = "Not run: the turn was cancelled before anyone answered.";
|
|
16
|
+
export const ASK_UNSHOWABLE_REASON = "Not run: the command contains a secret that cannot be shown to the approver safely; pass it through an environment variable instead.";
|
|
17
|
+
/** Kept for compatibility with existing callers and tests. */
|
|
18
|
+
export const ASK_UNAVAILABLE_REASON = ASK_NO_CHANNEL_REASON;
|
|
19
|
+
/**
|
|
20
|
+
* The resolver a run gets when no channel is available: a chain with zero
|
|
21
|
+
* links, whose terminal deny supplies the answer. TOTAL -- never abstains.
|
|
22
|
+
*/
|
|
23
|
+
export function headlessAskResolver() {
|
|
24
|
+
return chainAskLinks([]);
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexer and segmenter for a model-authored Bash command (spec §4 US-005.1-2).
|
|
3
|
+
*
|
|
4
|
+
* Safe-by-refusal, not safe-by-sandbox: any construct this lexer does not
|
|
5
|
+
* MODEL is refused by name, because a payload the gate cannot read is a
|
|
6
|
+
* payload no shell gets (spec R11). That is a deliberately small language —
|
|
7
|
+
* words, quotes, operators, simple redirections — and growing it is a
|
|
8
|
+
* permission decision, not a parser improvement.
|
|
9
|
+
*
|
|
10
|
+
* "Refused by name" is the whole guarantee, so it must hold for EVERY
|
|
11
|
+
* unmodelled construct, not only the ones that look dangerous. A construct
|
|
12
|
+
* that folds into a word instead is worse than one that is modelled: the
|
|
13
|
+
* segment still reaches the policy, and its first token is no longer the
|
|
14
|
+
* command a deny rule was written against.
|
|
15
|
+
*
|
|
16
|
+
* ZERO imports, on purpose. `src/permissions` must not value-import
|
|
17
|
+
* `@/tools` (the edge runs the other way: src/tools/runtime.ts imports this
|
|
18
|
+
* package), and a lexer that needs nothing is also a lexer that can be tested
|
|
19
|
+
* without a filesystem, a policy or a spawn.
|
|
20
|
+
*/
|
|
21
|
+
/** One word of a segment. `opaque` means it contained `$`-expansion, so its
|
|
22
|
+
* RUNTIME value is unknown here and policy-bash.ts refuses it before execution. */
|
|
23
|
+
export interface BashToken {
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly opaque: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A simple redirection. The target is containment-checked by the caller. */
|
|
28
|
+
export interface BashRedirect {
|
|
29
|
+
readonly operator: string;
|
|
30
|
+
readonly target: string;
|
|
31
|
+
readonly opaque: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The control operator that follows a command segment, when any. */
|
|
34
|
+
export type BashSegmentSeparator = ";" | "&&" | "||" | "|" | "&";
|
|
35
|
+
/** One command between control operators. */
|
|
36
|
+
export interface BashSegment {
|
|
37
|
+
readonly tokens: readonly BashToken[];
|
|
38
|
+
readonly redirects: readonly BashRedirect[];
|
|
39
|
+
readonly separator?: BashSegmentSeparator;
|
|
40
|
+
}
|
|
41
|
+
export type BashLexResult = {
|
|
42
|
+
readonly kind: "ok";
|
|
43
|
+
readonly segments: readonly BashSegment[];
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "refused";
|
|
46
|
+
readonly construct: string;
|
|
47
|
+
/**
|
|
48
|
+
* The lexable prefix at the point of refusal: the completed segments,
|
|
49
|
+
* followed by the interrupted segment's completed tokens and redirects
|
|
50
|
+
* only. The word being built when the refusal struck is dropped, as is a
|
|
51
|
+
* redirect operator still waiting for its target; a trailing segment with
|
|
52
|
+
* neither tokens nor redirects is omitted. `[]` when the refusal precedes
|
|
53
|
+
* any completed word. The gate runs its deny and payload checks over this
|
|
54
|
+
* prefix, so an out-of-bounds command never reaches the human just because
|
|
55
|
+
* a later construct was unreadable.
|
|
56
|
+
*/
|
|
57
|
+
readonly prefix: readonly BashSegment[];
|
|
58
|
+
};
|
|
59
|
+
export declare function lexBashCommand(command: string): BashLexResult;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexer and segmenter for a model-authored Bash command (spec §4 US-005.1-2).
|
|
3
|
+
*
|
|
4
|
+
* Safe-by-refusal, not safe-by-sandbox: any construct this lexer does not
|
|
5
|
+
* MODEL is refused by name, because a payload the gate cannot read is a
|
|
6
|
+
* payload no shell gets (spec R11). That is a deliberately small language —
|
|
7
|
+
* words, quotes, operators, simple redirections — and growing it is a
|
|
8
|
+
* permission decision, not a parser improvement.
|
|
9
|
+
*
|
|
10
|
+
* "Refused by name" is the whole guarantee, so it must hold for EVERY
|
|
11
|
+
* unmodelled construct, not only the ones that look dangerous. A construct
|
|
12
|
+
* that folds into a word instead is worse than one that is modelled: the
|
|
13
|
+
* segment still reaches the policy, and its first token is no longer the
|
|
14
|
+
* command a deny rule was written against.
|
|
15
|
+
*
|
|
16
|
+
* ZERO imports, on purpose. `src/permissions` must not value-import
|
|
17
|
+
* `@/tools` (the edge runs the other way: src/tools/runtime.ts imports this
|
|
18
|
+
* package), and a lexer that needs nothing is also a lexer that can be tested
|
|
19
|
+
* without a filesystem, a policy or a spawn.
|
|
20
|
+
*/
|
|
21
|
+
/** End index of a double-quoted run starting at `from`, honouring backslash
|
|
22
|
+
* escapes, or -1 when the quote is never closed. */
|
|
23
|
+
function doubleQuoteEnd(command, from) {
|
|
24
|
+
for (let i = from; i < command.length; i += 1) {
|
|
25
|
+
const char = command[i];
|
|
26
|
+
if (char === "\\") {
|
|
27
|
+
i += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === '"')
|
|
31
|
+
return i;
|
|
32
|
+
}
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
export function lexBashCommand(command) {
|
|
36
|
+
// Checked up front so the reason names the real problem. Leaving it to the
|
|
37
|
+
// final flushSegment() would report "an empty command segment", which is
|
|
38
|
+
// true and useless.
|
|
39
|
+
if (command.trim() === "")
|
|
40
|
+
return { kind: "refused", construct: "an empty command", prefix: [] };
|
|
41
|
+
const state = {
|
|
42
|
+
command,
|
|
43
|
+
segments: [],
|
|
44
|
+
tokens: [],
|
|
45
|
+
redirects: [],
|
|
46
|
+
word: "",
|
|
47
|
+
opaque: false,
|
|
48
|
+
started: false,
|
|
49
|
+
pendingRedirect: undefined,
|
|
50
|
+
};
|
|
51
|
+
let i = 0;
|
|
52
|
+
while (i < command.length) {
|
|
53
|
+
const step = stepAt(state, i);
|
|
54
|
+
if (step === undefined) {
|
|
55
|
+
// Plain word character: accumulates into the token being built.
|
|
56
|
+
state.word += command[i];
|
|
57
|
+
state.started = true;
|
|
58
|
+
i += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (step.kind === "refuse")
|
|
62
|
+
return refusedHere(state, step.construct);
|
|
63
|
+
i += step.by;
|
|
64
|
+
}
|
|
65
|
+
const error = flushSegment(state);
|
|
66
|
+
if (error !== undefined)
|
|
67
|
+
return refusedHere(state, error);
|
|
68
|
+
return { kind: "ok", segments: state.segments };
|
|
69
|
+
}
|
|
70
|
+
/** Snapshots the lexable prefix at a refusal site (see `BashLexResult`). The
|
|
71
|
+
* in-progress `word` is deliberately excluded, and `pendingRedirect` -- an
|
|
72
|
+
* operator awaiting its target -- lives outside `redirects`, so it is dropped
|
|
73
|
+
* with it. */
|
|
74
|
+
function refusedHere(state, construct) {
|
|
75
|
+
const prefix = state.tokens.length === 0 && state.redirects.length === 0
|
|
76
|
+
? [...state.segments]
|
|
77
|
+
: [...state.segments, { tokens: state.tokens, redirects: state.redirects }];
|
|
78
|
+
return { kind: "refused", construct, prefix };
|
|
79
|
+
}
|
|
80
|
+
function flushWord(state) {
|
|
81
|
+
if (!state.started)
|
|
82
|
+
return;
|
|
83
|
+
if (state.pendingRedirect !== undefined) {
|
|
84
|
+
state.redirects.push({ operator: state.pendingRedirect, target: state.word, opaque: state.opaque });
|
|
85
|
+
state.pendingRedirect = undefined;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
state.tokens.push({ text: state.word, opaque: state.opaque });
|
|
89
|
+
}
|
|
90
|
+
state.word = "";
|
|
91
|
+
state.opaque = false;
|
|
92
|
+
state.started = false;
|
|
93
|
+
}
|
|
94
|
+
/** Closes a segment, or names why it cannot be closed. A dangling operator
|
|
95
|
+
* leaves an empty segment, which is refused rather than silently dropped:
|
|
96
|
+
* `bun test &&` is a truncated command, and guessing at intent here would
|
|
97
|
+
* approve something nobody wrote. */
|
|
98
|
+
function flushSegment(state, separator) {
|
|
99
|
+
flushWord(state);
|
|
100
|
+
if (state.pendingRedirect !== undefined)
|
|
101
|
+
return "a redirection with no target";
|
|
102
|
+
if (state.tokens.length === 0 && state.redirects.length === 0)
|
|
103
|
+
return "an empty command segment";
|
|
104
|
+
state.segments.push({
|
|
105
|
+
tokens: state.tokens,
|
|
106
|
+
redirects: state.redirects,
|
|
107
|
+
...(separator === undefined ? {} : { separator }),
|
|
108
|
+
});
|
|
109
|
+
state.tokens = [];
|
|
110
|
+
state.redirects = [];
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function lexSingleQuote(state, i) {
|
|
114
|
+
if (state.command[i] !== "'")
|
|
115
|
+
return undefined;
|
|
116
|
+
const end = state.command.indexOf("'", i + 1);
|
|
117
|
+
if (end === -1)
|
|
118
|
+
return { kind: "refuse", construct: "an unbalanced single quote" };
|
|
119
|
+
// Single quotes suppress every expansion, so the content stays literal
|
|
120
|
+
// and the token stays analysable.
|
|
121
|
+
state.word += state.command.slice(i + 1, end);
|
|
122
|
+
state.started = true;
|
|
123
|
+
return { kind: "advance", by: end + 1 - i };
|
|
124
|
+
}
|
|
125
|
+
function lexDoubleQuote(state, i) {
|
|
126
|
+
if (state.command[i] !== '"')
|
|
127
|
+
return undefined;
|
|
128
|
+
const end = doubleQuoteEnd(state.command, i + 1);
|
|
129
|
+
if (end === -1)
|
|
130
|
+
return { kind: "refuse", construct: "an unbalanced double quote" };
|
|
131
|
+
const inner = state.command.slice(i + 1, end);
|
|
132
|
+
if (inner.includes("$("))
|
|
133
|
+
return { kind: "refuse", construct: "a command substitution `$(...)`" };
|
|
134
|
+
if (inner.includes("`"))
|
|
135
|
+
return { kind: "refuse", construct: "a backtick command substitution" };
|
|
136
|
+
if (inner.includes("$"))
|
|
137
|
+
state.opaque = true;
|
|
138
|
+
state.word += inner;
|
|
139
|
+
state.started = true;
|
|
140
|
+
return { kind: "advance", by: end + 1 - i };
|
|
141
|
+
}
|
|
142
|
+
function lexEscape(state, i) {
|
|
143
|
+
if (state.command[i] !== "\\")
|
|
144
|
+
return undefined;
|
|
145
|
+
const next = state.command[i + 1];
|
|
146
|
+
if (next === undefined)
|
|
147
|
+
return { kind: "refuse", construct: "a trailing backslash" };
|
|
148
|
+
state.word += next;
|
|
149
|
+
state.started = true;
|
|
150
|
+
return { kind: "advance", by: 2 };
|
|
151
|
+
}
|
|
152
|
+
/** Grouping, negation, comments and command substitutions are shell SYNTAX,
|
|
153
|
+
* not word characters: left unmodelled they fold into the token text, so
|
|
154
|
+
* `(rm -rf x)` and `! rm -rf x` present a first token of `(rm` / `!` that no
|
|
155
|
+
* `Bash(rm*)` deny rule can match, while /bin/sh runs the `rm` regardless.
|
|
156
|
+
* Refused by name, like every other construct this lexer cannot read. `!` and
|
|
157
|
+
* `#` are only special at the START of a word -- `a!b` and `a#b` are ordinary
|
|
158
|
+
* literals in sh, and refusing those would deny commands a grant plainly
|
|
159
|
+
* covers. */
|
|
160
|
+
function refuseGroupingOrExpansion(state, i) {
|
|
161
|
+
const command = state.command;
|
|
162
|
+
const char = command[i];
|
|
163
|
+
const next = command[i + 1];
|
|
164
|
+
if (char === "(" || char === ")")
|
|
165
|
+
return { kind: "refuse", construct: "a subshell `( ... )`" };
|
|
166
|
+
if (char === "!" && !state.started)
|
|
167
|
+
return { kind: "refuse", construct: "a `!` negation" };
|
|
168
|
+
if (char === "#" && !state.started)
|
|
169
|
+
return { kind: "refuse", construct: "a `#` comment" };
|
|
170
|
+
if (char === "$" && next === "(")
|
|
171
|
+
return { kind: "refuse", construct: "a command substitution `$(...)`" };
|
|
172
|
+
if (char === "`")
|
|
173
|
+
return { kind: "refuse", construct: "a backtick command substitution" };
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
/** Redirection FORMS this lexer does not model: a process substitution, a
|
|
177
|
+
* here-document, file-descriptor duplication and bash's `&>` shorthand are
|
|
178
|
+
* each refused by name before the plain `<`/`>` handler can claim them. */
|
|
179
|
+
function refuseRedirectForm(state, i) {
|
|
180
|
+
const command = state.command;
|
|
181
|
+
const char = command[i];
|
|
182
|
+
const next = command[i + 1];
|
|
183
|
+
if ((char === "<" || char === ">") && next === "(") {
|
|
184
|
+
return { kind: "refuse", construct: "a process substitution `<(...)` / `>(...)`" };
|
|
185
|
+
}
|
|
186
|
+
if (char === "<" && next === "<")
|
|
187
|
+
return { kind: "refuse", construct: "a here-document `<<`" };
|
|
188
|
+
if ((char === "<" || char === ">") && next === "&") {
|
|
189
|
+
return { kind: "refuse", construct: "file-descriptor duplication (`2>&1`)" };
|
|
190
|
+
}
|
|
191
|
+
if (char === "&" && next === ">")
|
|
192
|
+
return { kind: "refuse", construct: "the `&>` redirection form" };
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
function lexDollar(state, i) {
|
|
196
|
+
if (state.command[i] !== "$")
|
|
197
|
+
return undefined;
|
|
198
|
+
state.opaque = true;
|
|
199
|
+
state.word += "$";
|
|
200
|
+
state.started = true;
|
|
201
|
+
return { kind: "advance", by: 1 };
|
|
202
|
+
}
|
|
203
|
+
function skipWhitespace(state, i) {
|
|
204
|
+
const char = state.command[i];
|
|
205
|
+
if (char !== " " && char !== "\t" && char !== "\r")
|
|
206
|
+
return undefined;
|
|
207
|
+
flushWord(state);
|
|
208
|
+
return { kind: "advance", by: 1 };
|
|
209
|
+
}
|
|
210
|
+
function lexLineSeparator(state, i) {
|
|
211
|
+
const char = state.command[i];
|
|
212
|
+
if (char !== "\n" && char !== ";")
|
|
213
|
+
return undefined;
|
|
214
|
+
const error = flushSegment(state, ";");
|
|
215
|
+
if (error !== undefined)
|
|
216
|
+
return { kind: "refuse", construct: error };
|
|
217
|
+
return { kind: "advance", by: 1 };
|
|
218
|
+
}
|
|
219
|
+
function lexControlOperator(state, i) {
|
|
220
|
+
const command = state.command;
|
|
221
|
+
const char = command[i];
|
|
222
|
+
const next = command[i + 1];
|
|
223
|
+
if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
|
|
224
|
+
const error = flushSegment(state, char === "&" ? "&&" : "||");
|
|
225
|
+
if (error !== undefined)
|
|
226
|
+
return { kind: "refuse", construct: error };
|
|
227
|
+
return { kind: "advance", by: 2 };
|
|
228
|
+
}
|
|
229
|
+
if (char === "|" || char === "&") {
|
|
230
|
+
const error = flushSegment(state, char);
|
|
231
|
+
if (error !== undefined)
|
|
232
|
+
return { kind: "refuse", construct: error };
|
|
233
|
+
return { kind: "advance", by: 1 };
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
function lexRedirect(state, i) {
|
|
238
|
+
const command = state.command;
|
|
239
|
+
const char = command[i];
|
|
240
|
+
if (char !== ">" && char !== "<")
|
|
241
|
+
return undefined;
|
|
242
|
+
// A bare fd digit belongs to the operator (`2>err.txt`), not to argv:
|
|
243
|
+
// left in the token list it would have to satisfy an allow rule, and
|
|
244
|
+
// `Bash(bun test *)` would refuse a command it plainly covers.
|
|
245
|
+
if (/^\d$/.test(state.word)) {
|
|
246
|
+
state.word = "";
|
|
247
|
+
state.started = false;
|
|
248
|
+
}
|
|
249
|
+
flushWord(state);
|
|
250
|
+
const appends = char === ">" && command[i + 1] === ">";
|
|
251
|
+
state.pendingRedirect = appends ? ">>" : char;
|
|
252
|
+
return { kind: "advance", by: appends ? 2 : 1 };
|
|
253
|
+
}
|
|
254
|
+
/** The dispatch table. ORDER IS BEHAVIOUR: the first handler that claims the
|
|
255
|
+
* character wins, so the table must stay in the original guard chain's order —
|
|
256
|
+
* quote/escape handling, structural refusals, redirect-form refusals, `$`,
|
|
257
|
+
* whitespace, separators, plain redirection. */
|
|
258
|
+
const HANDLERS = [
|
|
259
|
+
lexSingleQuote,
|
|
260
|
+
lexDoubleQuote,
|
|
261
|
+
lexEscape,
|
|
262
|
+
refuseGroupingOrExpansion,
|
|
263
|
+
refuseRedirectForm,
|
|
264
|
+
lexDollar,
|
|
265
|
+
skipWhitespace,
|
|
266
|
+
lexLineSeparator,
|
|
267
|
+
lexControlOperator,
|
|
268
|
+
lexRedirect,
|
|
269
|
+
];
|
|
270
|
+
/** Runs the first handler that claims the character at `i`, or `undefined`
|
|
271
|
+
* when it is a plain word character (accumulated by `lexBashCommand`). */
|
|
272
|
+
function stepAt(state, i) {
|
|
273
|
+
for (const lex of HANDLERS) {
|
|
274
|
+
const step = lex(state, i);
|
|
275
|
+
if (step !== undefined)
|
|
276
|
+
return step;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolGrant } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Parse one #374 tool expression.
|
|
4
|
+
*
|
|
5
|
+
* `Read` -> unconditional
|
|
6
|
+
* `Write(src/**,test/**)` -> those two globs
|
|
7
|
+
* `Git(diff,log)` -> those two subcommands
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseToolExpression(expression: string): ToolGrant;
|
|
10
|
+
/** Parse a list of rule expressions into grants. Effect is carried by which
|
|
11
|
+
* config list (allow/deny/ask) the expressions came from, not by the grant. */
|
|
12
|
+
export declare function parseRuleList(expressions: readonly string[]): ToolGrant[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse one #374 tool expression.
|
|
3
|
+
*
|
|
4
|
+
* `Read` -> unconditional
|
|
5
|
+
* `Write(src/**,test/**)` -> those two globs
|
|
6
|
+
* `Git(diff,log)` -> those two subcommands
|
|
7
|
+
*/
|
|
8
|
+
export function parseToolExpression(expression) {
|
|
9
|
+
const open = expression.indexOf("(");
|
|
10
|
+
if (open === -1)
|
|
11
|
+
return { tool: expression.trim(), patterns: ["*"] };
|
|
12
|
+
const tool = expression.slice(0, open).trim();
|
|
13
|
+
const inner = expression.slice(open + 1, expression.lastIndexOf(")"));
|
|
14
|
+
const patterns = inner
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((p) => p.trim())
|
|
17
|
+
.filter((p) => p.length > 0);
|
|
18
|
+
return { tool, patterns: patterns.length > 0 ? patterns : ["*"] };
|
|
19
|
+
}
|
|
20
|
+
/** Parse a list of rule expressions into grants. Effect is carried by which
|
|
21
|
+
* config list (allow/deny/ask) the expressions came from, not by the grant. */
|
|
22
|
+
export function parseRuleList(expressions) {
|
|
23
|
+
return expressions.map(parseToolExpression);
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./approval-audit.ts";
|
|
2
|
+
export * from "./approvals-link.ts";
|
|
3
|
+
export * from "./approvals-store.ts";
|
|
4
|
+
export * from "./approvals-taint.ts";
|
|
5
|
+
export { ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, headlessAskResolver, } from "./ask.ts";
|
|
6
|
+
export * from "./ask-chain.ts";
|
|
7
|
+
export type { BashLexResult, BashRedirect, BashSegment, BashSegmentSeparator, BashToken } from "./bash-lex.ts";
|
|
8
|
+
export { lexBashCommand } from "./bash-lex.ts";
|
|
9
|
+
export { parseRuleList, parseToolExpression } from "./grammar.ts";
|
|
10
|
+
export * from "./secret-spans.ts";
|
|
11
|
+
export type { AskRequest, ResolvedPermissions } from "./types.ts";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./approval-audit.js";
|
|
2
|
+
export * from "./approvals-link.js";
|
|
3
|
+
export * from "./approvals-store.js";
|
|
4
|
+
export * from "./approvals-taint.js";
|
|
5
|
+
export { ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, headlessAskResolver, } from "./ask.js";
|
|
6
|
+
export * from "./ask-chain.js";
|
|
7
|
+
export { lexBashCommand } from "./bash-lex.js";
|
|
8
|
+
export { parseRuleList, parseToolExpression } from "./grammar.js";
|
|
9
|
+
export * from "./secret-spans.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SecretSpan {
|
|
2
|
+
readonly start: number;
|
|
3
|
+
readonly end: number;
|
|
4
|
+
readonly kind: string;
|
|
5
|
+
}
|
|
6
|
+
export type MaskResult = {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly masked: string;
|
|
9
|
+
readonly count: number;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
};
|
|
14
|
+
/** Non-overlapping spans, sorted; overlapping or touching matches merge (earliest kind wins). */
|
|
15
|
+
export declare function findSecretSpans(text: string): readonly SecretSpan[];
|
|
16
|
+
export declare function maskForPrompt(text: string): MaskResult;
|
|
17
|
+
export declare function redactForRow(text: string): string;
|
|
18
|
+
/** Apply `redactForRow` to every string leaf. Keys and non-string values are kept as-is. */
|
|
19
|
+
export declare function redactRowStrings<T>(value: T): T;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret spans in agent-authored command text (review #9, spec section 1).
|
|
3
|
+
*
|
|
4
|
+
* Two consumers with different stakes:
|
|
5
|
+
* - `maskForPrompt` feeds an approval prompt. A masked span must never hide
|
|
6
|
+
* code from the human approving it, so any span containing shell syntax
|
|
7
|
+
* makes the whole text unshowable and the ask is denied instead (D18).
|
|
8
|
+
* - `redactForRow` feeds local audit rows. Nothing is approved from them but
|
|
9
|
+
* forensic content matters, so each span is masked only up to its first
|
|
10
|
+
* shell-active character: `Cookie: a=b'; rm -rf ~` keeps `'; rm -rf ~`.
|
|
11
|
+
* Accepted residual: secret characters after a shell-active character stay
|
|
12
|
+
* visible in the row.
|
|
13
|
+
*/
|
|
14
|
+
import { SECRET_VALUE_PATTERNS } from "#src/internal/redact";
|
|
15
|
+
const SHELL_ACTIVE = /[$`;|&<>()'"\n]/;
|
|
16
|
+
/** A PEM body is newline-separated by construction; only real shell syntax cuts it. */
|
|
17
|
+
const SHELL_ACTIVE_IN_PEM = /[$`;|&<>()'"]/;
|
|
18
|
+
/** Kinds whose match is `NAME=value` / `name: value`; a `$` value is a reference, not a secret. */
|
|
19
|
+
const VALUE_KINDS = new Set(["assignment", "api-key-header"]);
|
|
20
|
+
function referencesVariable(kind, match) {
|
|
21
|
+
if (!VALUE_KINDS.has(kind))
|
|
22
|
+
return false;
|
|
23
|
+
const sep = match.search(/[=:]/);
|
|
24
|
+
return (sep >= 0 &&
|
|
25
|
+
match
|
|
26
|
+
.slice(sep + 1)
|
|
27
|
+
.trimStart()
|
|
28
|
+
.startsWith("$"));
|
|
29
|
+
}
|
|
30
|
+
function rawSpans(text) {
|
|
31
|
+
return SECRET_VALUE_PATTERNS.flatMap(({ kind, re }) => {
|
|
32
|
+
// A private copy: the shared /g regex carries lastIndex between callers.
|
|
33
|
+
const scan = new RegExp(re.source, re.flags);
|
|
34
|
+
const found = [];
|
|
35
|
+
for (let m = scan.exec(text); m !== null; m = scan.exec(text)) {
|
|
36
|
+
if (!referencesVariable(kind, m[0]))
|
|
37
|
+
found.push({ start: m.index, end: m.index + m[0].length, kind });
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Non-overlapping spans, sorted; overlapping or touching matches merge (earliest kind wins). */
|
|
43
|
+
export function findSecretSpans(text) {
|
|
44
|
+
const sorted = [...rawSpans(text)].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
45
|
+
const merged = [];
|
|
46
|
+
for (const span of sorted) {
|
|
47
|
+
const last = merged.at(-1);
|
|
48
|
+
if (last !== undefined && span.start <= last.end) {
|
|
49
|
+
merged[merged.length - 1] = { ...last, end: Math.max(last.end, span.end) };
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
merged.push(span);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return merged;
|
|
56
|
+
}
|
|
57
|
+
function replaceSpans(text, spans, render) {
|
|
58
|
+
const parts = spans.map((span, i) => {
|
|
59
|
+
const gapStart = i === 0 ? 0 : (spans[i - 1]?.end ?? 0);
|
|
60
|
+
return text.slice(gapStart, span.start) + render(span, text.slice(span.start, span.end));
|
|
61
|
+
});
|
|
62
|
+
return parts.join("") + text.slice(spans.at(-1)?.end ?? 0);
|
|
63
|
+
}
|
|
64
|
+
export function maskForPrompt(text) {
|
|
65
|
+
const spans = findSecretSpans(text);
|
|
66
|
+
const unsafe = spans.find((span) => SHELL_ACTIVE.test(text.slice(span.start, span.end)));
|
|
67
|
+
if (unsafe !== undefined) {
|
|
68
|
+
return { ok: false, reason: `a ${unsafe.kind} secret spans shell syntax` };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, masked: replaceSpans(text, spans, (span) => `[REDACTED:${span.kind}]`), count: spans.length };
|
|
71
|
+
}
|
|
72
|
+
export function redactForRow(text) {
|
|
73
|
+
return replaceSpans(text, findSecretSpans(text), (span, body) => {
|
|
74
|
+
const cut = body.search(span.kind === "pem" ? SHELL_ACTIVE_IN_PEM : SHELL_ACTIVE);
|
|
75
|
+
return cut === -1 ? `[REDACTED:${span.kind}]` : `[REDACTED:${span.kind}]${body.slice(cut)}`;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Apply `redactForRow` to every string leaf. Keys and non-string values are kept as-is. */
|
|
79
|
+
export function redactRowStrings(value) {
|
|
80
|
+
if (typeof value === "string")
|
|
81
|
+
return redactForRow(value);
|
|
82
|
+
if (Array.isArray(value))
|
|
83
|
+
return value.map((item) => redactRowStrings(item));
|
|
84
|
+
if (value !== null && typeof value === "object") {
|
|
85
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactRowStrings(v)]));
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|