@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,168 @@
|
|
|
1
|
+
export interface ApprovalEntry {
|
|
2
|
+
readonly stage: string;
|
|
3
|
+
readonly command: string;
|
|
4
|
+
readonly root: string;
|
|
5
|
+
readonly origin: "escalate" | "askRule";
|
|
6
|
+
readonly matchedRule: string | null;
|
|
7
|
+
readonly approvedAt: string;
|
|
8
|
+
readonly approvedBy: string;
|
|
9
|
+
readonly naxCommit: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Written by nax itself when a FORGE-CAPABLE run uses the store (#2199): every
|
|
13
|
+
* entry beside it is untrusted, and the next run that trusts the cache
|
|
14
|
+
* discards them. See approvals-taint.ts for the trust argument.
|
|
15
|
+
*/
|
|
16
|
+
export interface ApprovalsTaint {
|
|
17
|
+
readonly since: string;
|
|
18
|
+
readonly runId: string;
|
|
19
|
+
/** The tainting nax process; `undefined` when absent or malformed on disk. */
|
|
20
|
+
readonly pid: number | undefined;
|
|
21
|
+
}
|
|
22
|
+
export interface ApprovalsFile {
|
|
23
|
+
readonly entries: readonly ApprovalEntry[];
|
|
24
|
+
readonly taint: ApprovalsTaint | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The classification a `readApprovalsFileDetailed` call reports. `missing` is a
|
|
28
|
+
* path that resolves to no file at all (ENOENT, or a parent dir that does not
|
|
29
|
+
* exist); `unparseable` is a file whose contents the read could not turn into
|
|
30
|
+
* the entries+taint shape the cache needs; `ok` is a file the read accepts.
|
|
31
|
+
*/
|
|
32
|
+
export type ApprovalsFileState = "ok" | "missing" | "unparseable";
|
|
33
|
+
/**
|
|
34
|
+
* What `readApprovalsFileDetailed` returns: the same `ApprovalsFile` shape
|
|
35
|
+
* `readApprovalsFile` always has, plus a classification and the count of array
|
|
36
|
+
* elements the read dropped for not being approval entries. `droppedMalformed`
|
|
37
|
+
* is `0` unless `state === "ok"` -- an unparseable file has no entries to
|
|
38
|
+
* drop, and a missing file has no file at all.
|
|
39
|
+
*/
|
|
40
|
+
export interface ApprovalsFileRead {
|
|
41
|
+
readonly file: ApprovalsFile;
|
|
42
|
+
readonly state: ApprovalsFileState;
|
|
43
|
+
readonly droppedMalformed: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The run's output dir, NOT the tool root. `root` is storyExecRoot -- the repo
|
|
47
|
+
* OR WORKTREE root -- so a root-relative file is deleted by the
|
|
48
|
+
* `git worktree remove --force` that ends the run. That is the exact defect
|
|
49
|
+
* recorded in toolAuditDir's docblock (src/config/paths/index.ts:138-150).
|
|
50
|
+
*/
|
|
51
|
+
export declare function approvalsPath(outputDir: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Stable, derived id for an entry. The id is a function of (stage, command,
|
|
54
|
+
* approvedAt) alone -- NOT of the entry's position in the file, or of any
|
|
55
|
+
* sibling entry -- so it survives other entries being added or removed and
|
|
56
|
+
* two entries recorded for the same triple share an id and are removed
|
|
57
|
+
* together (US-001).
|
|
58
|
+
*
|
|
59
|
+
* Computed on every read and never stored. A missing or non-string
|
|
60
|
+
* `approvedAt` reads as the empty string so the id is still a well-defined
|
|
61
|
+
* 8-character hex value for the malformed-on-disk entries the cache admits.
|
|
62
|
+
*/
|
|
63
|
+
export declare function approvalId(entry: ApprovalEntry): string;
|
|
64
|
+
/**
|
|
65
|
+
* Classify a store read. `missing` means the file is absent (or its parent
|
|
66
|
+
* directory is); `unparseable` means a file is present but its contents are
|
|
67
|
+
* not a `{ entries, taint }` object the cache can trust; `ok` means the read
|
|
68
|
+
* accepted the file and `file.entries` lists every element `isApprovalEntry`
|
|
69
|
+
* accepted, with `droppedMalformed` counting the rejections.
|
|
70
|
+
*
|
|
71
|
+
* A present `taint` is still parsed when the rest of the file is unparseable:
|
|
72
|
+
* a forged `entries` array must not strip the marker.
|
|
73
|
+
*
|
|
74
|
+
* A read failure (EACCES/EIO) is not a parse failure and is not classified
|
|
75
|
+
* here — it propagates so a caller that must surface it (the `nax approvals`
|
|
76
|
+
* CLI) can report it instead of mislabelling it `unparseable`. Callers that
|
|
77
|
+
* must stay fail-closed use `readApprovalsFile`, which catches it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function readApprovalsFileDetailed(path: string): Promise<ApprovalsFileRead>;
|
|
80
|
+
/**
|
|
81
|
+
* Missing, malformed OR unreadable reads as empty: the CACHE fails, the chain
|
|
82
|
+
* does not. A permissions/IO error is caught here (rather than propagating
|
|
83
|
+
* from `readApprovalsFileDetailed`) so it cannot abort an approval lookup; a
|
|
84
|
+
* caller that must distinguish it calls the detailed read directly.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readApprovalsFile(path: string): Promise<ApprovalsFile>;
|
|
87
|
+
export declare function readApprovals(path: string): Promise<readonly ApprovalEntry[]>;
|
|
88
|
+
/** Serialize the whole store. Callers hold the path lock. */
|
|
89
|
+
export declare function writeApprovalsFile(path: string, file: ApprovalsFile): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Lock the read-modify-write. Worktree-isolated parallel stories share one
|
|
92
|
+
* project-scoped file BY DESIGN (that is why `root` is not part of the key), so
|
|
93
|
+
* two "Allow + remember" taps can race and drop one. `withPathFileLock` is the
|
|
94
|
+
* shared primitive for exactly this shape; it fails closed on timeout rather
|
|
95
|
+
* than entering over a possible holder.
|
|
96
|
+
*
|
|
97
|
+
* The taint marker is PRESERVED: a "remember" tapped during a forge-capable run
|
|
98
|
+
* lands in a store that stays untrusted.
|
|
99
|
+
*/
|
|
100
|
+
export declare function appendApproval(path: string, entry: ApprovalEntry): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* The decision `removeApprovals` asks the caller for, once the read has
|
|
103
|
+
* classified the file. The caller picks which entries to drop with `remove` --
|
|
104
|
+
* a predicate over the kept entries from the read -- or refuses the operation
|
|
105
|
+
* with `refuse`. The two arms are mutually exclusive: a single decision is
|
|
106
|
+
* either "remove these" or "I will not act, because: ...".
|
|
107
|
+
*/
|
|
108
|
+
export type RemovalDecision = {
|
|
109
|
+
readonly remove: (entry: ApprovalEntry) => boolean;
|
|
110
|
+
} | {
|
|
111
|
+
readonly refuse: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* What `removeApprovals` reports back. `refused` covers both the file being
|
|
115
|
+
* unparseable (so the read refuses the call to `decide`) and the caller's own
|
|
116
|
+
* `refuse` decision: the caller cannot distinguish the two from the result
|
|
117
|
+
* alone, only from the message. `removed` reports exactly the entries the
|
|
118
|
+
* caller chose to drop plus the count of malformed array elements the read
|
|
119
|
+
* dropped for being non-entries. `unchanged` is returned for both a missing
|
|
120
|
+
* file and a present file whose predicate selected nothing.
|
|
121
|
+
*/
|
|
122
|
+
export type RemovalResult = {
|
|
123
|
+
readonly outcome: "removed";
|
|
124
|
+
readonly removed: readonly ApprovalEntry[];
|
|
125
|
+
readonly droppedMalformed: number;
|
|
126
|
+
} | {
|
|
127
|
+
readonly outcome: "unchanged";
|
|
128
|
+
} | {
|
|
129
|
+
readonly outcome: "refused";
|
|
130
|
+
readonly reason: string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Locked, taint-preserving removal. Holds the same path-scoped lock
|
|
134
|
+
* `appendApproval` holds, so a removal racing an append cannot drop one of the
|
|
135
|
+
* two writes; whichever runs first inside the lock, the other sees the
|
|
136
|
+
* post-write state on its next read.
|
|
137
|
+
*
|
|
138
|
+
* The order of the guards matters and is documented in the story:
|
|
139
|
+
* 1. `state === "unparseable"` -> `refused`, no `decide` call, no write.
|
|
140
|
+
* Rewriting would erase whatever the file holds, including a taint
|
|
141
|
+
* marker we did not write.
|
|
142
|
+
* 2. `decide(read)` returns `{ refuse }` -> `refused`, no write.
|
|
143
|
+
* 3. The predicate selected no entry (or the file was missing) ->
|
|
144
|
+
* `unchanged`, no write, and neither the data file nor its parent
|
|
145
|
+
* directory is created.
|
|
146
|
+
* 4. Otherwise write `{ taint: read.file.taint, entries: kept }` with the
|
|
147
|
+
* SAME taint that was read. Nothing in this function clears a taint;
|
|
148
|
+
* only `clearApprovalsTaint` (approvals-taint.ts) does, and only from a
|
|
149
|
+
* trusted run.
|
|
150
|
+
*
|
|
151
|
+
* The lock file must live next to the target, so the lock is only taken when
|
|
152
|
+
* that parent directory already exists. A path whose parent is missing is by
|
|
153
|
+
* definition a missing store: the read-decide path runs without the lock and
|
|
154
|
+
* creates nothing -- taking the lock there would require `mkdir`, which rule 3
|
|
155
|
+
* forbids. `decide` is still invoked exactly once in that arm, so the missing
|
|
156
|
+
* read reaches the caller as it does under the lock.
|
|
157
|
+
*/
|
|
158
|
+
export declare function removeApprovals(path: string, decide: (read: ApprovalsFileRead) => RemovalDecision): Promise<RemovalResult>;
|
|
159
|
+
/**
|
|
160
|
+
* BYTE-EXACT. No normalization. See the module docblock.
|
|
161
|
+
*
|
|
162
|
+
* `projectRoot`, when given, also requires the entry's `root` to be that root
|
|
163
|
+
* or lie inside it. Worktrees (`<projectRoot>/.nax-wt/<id>`) and monorepo
|
|
164
|
+
* package dirs both do, so parallel stories still share entries. This is
|
|
165
|
+
* HYGIENE, not authentication: it drops entries another project wrote into a
|
|
166
|
+
* shared outputDir, but a forger can write any `root` it likes (#2199).
|
|
167
|
+
*/
|
|
168
|
+
export declare function findApproval(entries: readonly ApprovalEntry[], stage: string, command: string, projectRoot?: string): ApprovalEntry | undefined;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable store for remembered approvals (P2 design section 6.6).
|
|
3
|
+
*
|
|
4
|
+
* A remembered approval is a CACHED HUMAN DECISION, not a rule. Lookup is
|
|
5
|
+
* BYTE-EXACT on (stage, command) with no normalization of any kind -- not
|
|
6
|
+
* trimming, not whitespace collapsing, not quote folding. Any normalization is
|
|
7
|
+
* a place where the string that was approved and the string that runs can
|
|
8
|
+
* diverge, and the whole value of the cache is that they cannot.
|
|
9
|
+
*
|
|
10
|
+
* Synthesizing a `Bash(...)` rule instead would be materially broader than what
|
|
11
|
+
* the operator approved: rule matching is a token-wise PREFIX match with no
|
|
12
|
+
* length ceiling (src/tools/policy-bash.ts:72-83), so a rule made from
|
|
13
|
+
* `bun run test` would also grant `bun run test --reporter=./x`.
|
|
14
|
+
*
|
|
15
|
+
* The file may also carry a `taint` marker (#2199) -- see approvals-taint.ts.
|
|
16
|
+
*
|
|
17
|
+
* Revocation goes through `src/cli/approvals.ts`, the surface D20 relies on:
|
|
18
|
+
* `removeApprovals` here is the locked, taint-preserving read-decide-write
|
|
19
|
+
* primitive that surface calls into.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
23
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
24
|
+
import { withPathFileLock } from "#src/internal/path-file-lock";
|
|
25
|
+
const EMPTY_FILE = { entries: [], taint: undefined };
|
|
26
|
+
/**
|
|
27
|
+
* The run's output dir, NOT the tool root. `root` is storyExecRoot -- the repo
|
|
28
|
+
* OR WORKTREE root -- so a root-relative file is deleted by the
|
|
29
|
+
* `git worktree remove --force` that ends the run. That is the exact defect
|
|
30
|
+
* recorded in toolAuditDir's docblock (src/config/paths/index.ts:138-150).
|
|
31
|
+
*/
|
|
32
|
+
export function approvalsPath(outputDir) {
|
|
33
|
+
return join(outputDir, "approvals.json");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The shape findApproval relies on. A malformed element (null, a string, an
|
|
37
|
+
* object missing `stage`/`command`) is DROPPED rather than returned: an
|
|
38
|
+
* unvalidated array makes `findApproval`'s `e.stage` throw, and a poisoned
|
|
39
|
+
* cache read would fail the whole ask chain instead of just this entry.
|
|
40
|
+
*/
|
|
41
|
+
function isApprovalEntry(value) {
|
|
42
|
+
if (typeof value !== "object" || value === null)
|
|
43
|
+
return false;
|
|
44
|
+
if (!("stage" in value) || !("command" in value))
|
|
45
|
+
return false;
|
|
46
|
+
return typeof value.stage === "string" && typeof value.command === "string";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* ANY present `taint` value taints, however malformed: the marker exists to
|
|
50
|
+
* withhold trust, so an unparseable one must not grant it.
|
|
51
|
+
*/
|
|
52
|
+
function parseTaint(value) {
|
|
53
|
+
if (value === undefined || value === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
const raw = typeof value === "object" ? { ...value } : {};
|
|
56
|
+
return {
|
|
57
|
+
since: typeof raw.since === "string" ? raw.since : "",
|
|
58
|
+
runId: typeof raw.runId === "string" ? raw.runId : "",
|
|
59
|
+
pid: typeof raw.pid === "number" && Number.isInteger(raw.pid) ? raw.pid : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Stable, derived id for an entry. The id is a function of (stage, command,
|
|
64
|
+
* approvedAt) alone -- NOT of the entry's position in the file, or of any
|
|
65
|
+
* sibling entry -- so it survives other entries being added or removed and
|
|
66
|
+
* two entries recorded for the same triple share an id and are removed
|
|
67
|
+
* together (US-001).
|
|
68
|
+
*
|
|
69
|
+
* Computed on every read and never stored. A missing or non-string
|
|
70
|
+
* `approvedAt` reads as the empty string so the id is still a well-defined
|
|
71
|
+
* 8-character hex value for the malformed-on-disk entries the cache admits.
|
|
72
|
+
*/
|
|
73
|
+
export function approvalId(entry) {
|
|
74
|
+
const approvedAt = typeof entry.approvedAt === "string" ? entry.approvedAt : "";
|
|
75
|
+
const digest = createHash("sha256");
|
|
76
|
+
digest.update(`${entry.stage}\0${entry.command}\0${approvedAt}`);
|
|
77
|
+
return digest.digest("hex").slice(0, 8);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Classify a store read. `missing` means the file is absent (or its parent
|
|
81
|
+
* directory is); `unparseable` means a file is present but its contents are
|
|
82
|
+
* not a `{ entries, taint }` object the cache can trust; `ok` means the read
|
|
83
|
+
* accepted the file and `file.entries` lists every element `isApprovalEntry`
|
|
84
|
+
* accepted, with `droppedMalformed` counting the rejections.
|
|
85
|
+
*
|
|
86
|
+
* A present `taint` is still parsed when the rest of the file is unparseable:
|
|
87
|
+
* a forged `entries` array must not strip the marker.
|
|
88
|
+
*
|
|
89
|
+
* A read failure (EACCES/EIO) is not a parse failure and is not classified
|
|
90
|
+
* here — it propagates so a caller that must surface it (the `nax approvals`
|
|
91
|
+
* CLI) can report it instead of mislabelling it `unparseable`. Callers that
|
|
92
|
+
* must stay fail-closed use `readApprovalsFile`, which catches it.
|
|
93
|
+
*/
|
|
94
|
+
export async function readApprovalsFileDetailed(path) {
|
|
95
|
+
let contents;
|
|
96
|
+
try {
|
|
97
|
+
contents = await readFile(path, "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
101
|
+
return { state: "missing", file: EMPTY_FILE, droppedMalformed: 0 };
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(contents);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { state: "unparseable", file: EMPTY_FILE, droppedMalformed: 0 };
|
|
111
|
+
}
|
|
112
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
113
|
+
return { state: "unparseable", file: EMPTY_FILE, droppedMalformed: 0 };
|
|
114
|
+
}
|
|
115
|
+
const { entries, taint } = parsed;
|
|
116
|
+
if ("entries" in parsed && !Array.isArray(entries)) {
|
|
117
|
+
return { state: "unparseable", file: { entries: [], taint: parseTaint(taint) }, droppedMalformed: 0 };
|
|
118
|
+
}
|
|
119
|
+
const array = Array.isArray(entries) ? entries : [];
|
|
120
|
+
const kept = [];
|
|
121
|
+
let dropped = 0;
|
|
122
|
+
for (const element of array) {
|
|
123
|
+
if (isApprovalEntry(element)) {
|
|
124
|
+
kept.push(element);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
dropped += 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { state: "ok", file: { entries: kept, taint: parseTaint(taint) }, droppedMalformed: dropped };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Missing, malformed OR unreadable reads as empty: the CACHE fails, the chain
|
|
134
|
+
* does not. A permissions/IO error is caught here (rather than propagating
|
|
135
|
+
* from `readApprovalsFileDetailed`) so it cannot abort an approval lookup; a
|
|
136
|
+
* caller that must distinguish it calls the detailed read directly.
|
|
137
|
+
*/
|
|
138
|
+
export async function readApprovalsFile(path) {
|
|
139
|
+
try {
|
|
140
|
+
return (await readApprovalsFileDetailed(path)).file;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return EMPTY_FILE;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export async function readApprovals(path) {
|
|
147
|
+
return (await readApprovalsFile(path)).entries;
|
|
148
|
+
}
|
|
149
|
+
/** Serialize the whole store. Callers hold the path lock. */
|
|
150
|
+
export async function writeApprovalsFile(path, file) {
|
|
151
|
+
const body = file.taint === undefined ? { entries: file.entries } : { taint: file.taint, entries: file.entries };
|
|
152
|
+
await mkdir(dirname(path), { recursive: true });
|
|
153
|
+
await writeFile(path, `${JSON.stringify(body, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Lock the read-modify-write. Worktree-isolated parallel stories share one
|
|
157
|
+
* project-scoped file BY DESIGN (that is why `root` is not part of the key), so
|
|
158
|
+
* two "Allow + remember" taps can race and drop one. `withPathFileLock` is the
|
|
159
|
+
* shared primitive for exactly this shape; it fails closed on timeout rather
|
|
160
|
+
* than entering over a possible holder.
|
|
161
|
+
*
|
|
162
|
+
* The taint marker is PRESERVED: a "remember" tapped during a forge-capable run
|
|
163
|
+
* lands in a store that stays untrusted.
|
|
164
|
+
*/
|
|
165
|
+
export async function appendApproval(path, entry) {
|
|
166
|
+
await mkdir(dirname(path), { recursive: true });
|
|
167
|
+
await withPathFileLock(path, async () => {
|
|
168
|
+
const existing = await readApprovalsFile(path);
|
|
169
|
+
await writeApprovalsFile(path, { taint: existing.taint, entries: [...existing.entries, entry] });
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The locked read-decide-write body shared by the two `removeApprovals` arms.
|
|
174
|
+
* Pure read -> guard -> decide -> filter -> write: no mkdir, no lock handling.
|
|
175
|
+
*/
|
|
176
|
+
async function applyRemoval(path, decide) {
|
|
177
|
+
const read = await readApprovalsFileDetailed(path);
|
|
178
|
+
if (read.state === "unparseable") {
|
|
179
|
+
return { outcome: "refused", reason: "approvals.json could not be parsed; not rewriting it" };
|
|
180
|
+
}
|
|
181
|
+
const decision = decide(read);
|
|
182
|
+
if ("refuse" in decision) {
|
|
183
|
+
return { outcome: "refused", reason: decision.refuse };
|
|
184
|
+
}
|
|
185
|
+
const removed = [];
|
|
186
|
+
const kept = [];
|
|
187
|
+
for (const entry of read.file.entries) {
|
|
188
|
+
if (decision.remove(entry)) {
|
|
189
|
+
removed.push(entry);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
kept.push(entry);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (removed.length === 0) {
|
|
196
|
+
return { outcome: "unchanged" };
|
|
197
|
+
}
|
|
198
|
+
await writeApprovalsFile(path, { taint: read.file.taint, entries: kept });
|
|
199
|
+
return { outcome: "removed", removed, droppedMalformed: read.droppedMalformed };
|
|
200
|
+
}
|
|
201
|
+
/** `true` iff `dir` exists and is a directory (symlinks followed). */
|
|
202
|
+
async function directoryExists(dir) {
|
|
203
|
+
try {
|
|
204
|
+
return (await stat(dir)).isDirectory();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Locked, taint-preserving removal. Holds the same path-scoped lock
|
|
212
|
+
* `appendApproval` holds, so a removal racing an append cannot drop one of the
|
|
213
|
+
* two writes; whichever runs first inside the lock, the other sees the
|
|
214
|
+
* post-write state on its next read.
|
|
215
|
+
*
|
|
216
|
+
* The order of the guards matters and is documented in the story:
|
|
217
|
+
* 1. `state === "unparseable"` -> `refused`, no `decide` call, no write.
|
|
218
|
+
* Rewriting would erase whatever the file holds, including a taint
|
|
219
|
+
* marker we did not write.
|
|
220
|
+
* 2. `decide(read)` returns `{ refuse }` -> `refused`, no write.
|
|
221
|
+
* 3. The predicate selected no entry (or the file was missing) ->
|
|
222
|
+
* `unchanged`, no write, and neither the data file nor its parent
|
|
223
|
+
* directory is created.
|
|
224
|
+
* 4. Otherwise write `{ taint: read.file.taint, entries: kept }` with the
|
|
225
|
+
* SAME taint that was read. Nothing in this function clears a taint;
|
|
226
|
+
* only `clearApprovalsTaint` (approvals-taint.ts) does, and only from a
|
|
227
|
+
* trusted run.
|
|
228
|
+
*
|
|
229
|
+
* The lock file must live next to the target, so the lock is only taken when
|
|
230
|
+
* that parent directory already exists. A path whose parent is missing is by
|
|
231
|
+
* definition a missing store: the read-decide path runs without the lock and
|
|
232
|
+
* creates nothing -- taking the lock there would require `mkdir`, which rule 3
|
|
233
|
+
* forbids. `decide` is still invoked exactly once in that arm, so the missing
|
|
234
|
+
* read reaches the caller as it does under the lock.
|
|
235
|
+
*/
|
|
236
|
+
export async function removeApprovals(path, decide) {
|
|
237
|
+
if (!(await directoryExists(dirname(path)))) {
|
|
238
|
+
return applyRemoval(path, decide);
|
|
239
|
+
}
|
|
240
|
+
return withPathFileLock(path, () => applyRemoval(path, decide));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* BYTE-EXACT. No normalization. See the module docblock.
|
|
244
|
+
*
|
|
245
|
+
* `projectRoot`, when given, also requires the entry's `root` to be that root
|
|
246
|
+
* or lie inside it. Worktrees (`<projectRoot>/.nax-wt/<id>`) and monorepo
|
|
247
|
+
* package dirs both do, so parallel stories still share entries. This is
|
|
248
|
+
* HYGIENE, not authentication: it drops entries another project wrote into a
|
|
249
|
+
* shared outputDir, but a forger can write any `root` it likes (#2199).
|
|
250
|
+
*/
|
|
251
|
+
export function findApproval(entries, stage, command, projectRoot) {
|
|
252
|
+
return entries.find((e) => e.stage === stage && e.command === command && (projectRoot === undefined || isWithin(projectRoot, e.root)));
|
|
253
|
+
}
|
|
254
|
+
function isWithin(projectRoot, root) {
|
|
255
|
+
if (typeof root !== "string" || root === "")
|
|
256
|
+
return false;
|
|
257
|
+
const rel = relative(resolve(projectRoot), resolve(root));
|
|
258
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
259
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const _approvalsTaintDeps: {
|
|
2
|
+
pid: () => number;
|
|
3
|
+
now: () => Date;
|
|
4
|
+
isProcessAlive: (pid: number) => boolean;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A run can forge the approvals file when any stage runs a raw shell outside
|
|
8
|
+
* the sandbox. With the sandbox enabled the file is always in `denyWrite`, and
|
|
9
|
+
* a raw stage is either wrapped or refused. Config-only: no probe dependency.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isForgeCapable(stageModes: readonly string[], sandboxEnabled: boolean): boolean;
|
|
12
|
+
/** Drop every entry and record who tainted the store. */
|
|
13
|
+
export declare function taintApprovals(path: string, runId: string): Promise<void>;
|
|
14
|
+
/** `held`: a still-running forge-capable nax process owns the taint. */
|
|
15
|
+
export type ClearTaintOutcome = "clean" | "cleared" | "held";
|
|
16
|
+
/**
|
|
17
|
+
* Start a trusted epoch. Entries that sat beside a taint are dropped, never
|
|
18
|
+
* promoted: nothing distinguishes a forged one from a human's.
|
|
19
|
+
*/
|
|
20
|
+
export declare function clearApprovalsTaint(path: string, runId: string): Promise<ClearTaintOutcome>;
|
|
21
|
+
export interface PrepareApprovalsStoreOptions {
|
|
22
|
+
readonly approvalsFile: string;
|
|
23
|
+
readonly runId: string;
|
|
24
|
+
/** Absent for a run-scoped (post-run) dispatch scope. */
|
|
25
|
+
readonly storyId?: string;
|
|
26
|
+
readonly forgeCapable: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Taint (forge-capable) or clear (trusted) the store. Never throws: a failed
|
|
30
|
+
* clear leaves the taint in place, so the link abstains and the cost is
|
|
31
|
+
* prompts; a failed taint is logged, since a later run may then trust entries.
|
|
32
|
+
*/
|
|
33
|
+
export declare function prepareApprovalsStore(opts: PrepareApprovalsStoreOptions): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-run provenance for the approvals cache (#2199).
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM. `approvals.json` is project-scoped and outlives the run. A run
|
|
5
|
+
* with any `bashApproval: raw` stage and no sandbox ("forge-capable") has a
|
|
6
|
+
* shell that can append arbitrary entries, and a LATER run that trusts the
|
|
7
|
+
* cache would auto-allow them as `decidedBy: "cache"`. Checking only the
|
|
8
|
+
* current run's stage modes cannot see that history.
|
|
9
|
+
*
|
|
10
|
+
* WHY NOT A SIGNATURE. An unsandboxed raw shell runs as the same OS user as
|
|
11
|
+
* nax. Any key nax can read from disk -- under ~/.nax or anywhere else -- that
|
|
12
|
+
* shell can read too, and then mint valid signatures. Signing would look
|
|
13
|
+
* stronger than it is, so it is deliberately not done (ADR-030 said the same).
|
|
14
|
+
*
|
|
15
|
+
* THE DESIGN: a taint marker that only nax writes, at points no agent runs.
|
|
16
|
+
* 1. A forge-capable run TAINTS the store before a dispatch scope's agents
|
|
17
|
+
* start (a story, or a post-run fix loop) and again after they finish: every entry is dropped and a `taint` record is
|
|
18
|
+
* written. The end-of-story re-taint wipes whatever the story's agents
|
|
19
|
+
* wrote, even an agent that knew to strip the first marker. One final seal
|
|
20
|
+
* runs at the end of the run (`buildApprovalsSeal`, awaited by `cleanupRun`
|
|
21
|
+
* and by the signal teardown), covering agents dispatched outside any
|
|
22
|
+
* dispatch-ask scope after the last per-scope re-taint.
|
|
23
|
+
* 2. The cache link ABSTAINS on a tainted store, whatever it contains.
|
|
24
|
+
* 3. A run that trusts the cache clears a taint before building its link by
|
|
25
|
+
* dropping every entry with it, so a trusted epoch starts empty. It leaves
|
|
26
|
+
* the taint alone while the tainting run may still be writing: the same
|
|
27
|
+
* run (a sibling story in a forge-capable package), or another nax process
|
|
28
|
+
* that is still alive.
|
|
29
|
+
*
|
|
30
|
+
* RESIDUAL RISK, stated so it is not overclaimed. The guarantee is against an
|
|
31
|
+
* agent that forges entries during its turn, not against a hostile one that
|
|
32
|
+
* understands this scheme. Out of reach of any on-disk mechanism:
|
|
33
|
+
* - a process the agent leaves running past its story (e.g. a detached
|
|
34
|
+
* `sleep; rewrite`) that strips the marker and forges after the last
|
|
35
|
+
* re-taint;
|
|
36
|
+
* - a forge-capable run killed by a death no handler sees (SIGKILL, power
|
|
37
|
+
* loss) between an agent stripping the marker and the run-end seal. Normal
|
|
38
|
+
* exits and handled signals both seal: `cleanupRun` awaits the seal in the
|
|
39
|
+
* runner's finally, and `performTeardown` awaits it after the PID sweep.
|
|
40
|
+
* An unsandboxed raw shell can equally edit shell rc files or git hooks, so
|
|
41
|
+
* this sits inside the posture ADR-030 already discloses; the sandbox, which
|
|
42
|
+
* always write-denies this file, is the real boundary.
|
|
43
|
+
*/
|
|
44
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
45
|
+
import { withPathFileLock } from "#src/internal/path-file-lock";
|
|
46
|
+
import { readApprovalsFile, writeApprovalsFile } from "./approvals-store.js";
|
|
47
|
+
/** `kill(pid, 0)` failing with EPERM means the process exists but is not ours. */
|
|
48
|
+
const ERRNO_NO_PERMISSION = "EPERM";
|
|
49
|
+
export const _approvalsTaintDeps = {
|
|
50
|
+
pid: () => process.pid,
|
|
51
|
+
now: () => new Date(),
|
|
52
|
+
isProcessAlive: (pid) => {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return err.code === ERRNO_NO_PERMISSION;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A run can forge the approvals file when any stage runs a raw shell outside
|
|
64
|
+
* the sandbox. With the sandbox enabled the file is always in `denyWrite`, and
|
|
65
|
+
* a raw stage is either wrapped or refused. Config-only: no probe dependency.
|
|
66
|
+
*/
|
|
67
|
+
export function isForgeCapable(stageModes, sandboxEnabled) {
|
|
68
|
+
return stageModes.includes("raw") && !sandboxEnabled;
|
|
69
|
+
}
|
|
70
|
+
/** Drop every entry and record who tainted the store. */
|
|
71
|
+
export async function taintApprovals(path, runId) {
|
|
72
|
+
const taint = { since: _approvalsTaintDeps.now().toISOString(), runId, pid: _approvalsTaintDeps.pid() };
|
|
73
|
+
await withPathFileLock(path, async () => writeApprovalsFile(path, { taint, entries: [] }));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether the tainting run may still be writing. A taint from THIS run is held:
|
|
77
|
+
* stories can resolve different sandbox settings per package, so a trusted
|
|
78
|
+
* story must not clear the marker a forge-capable sibling story just wrote. A
|
|
79
|
+
* taint from an earlier run in this same process is not held.
|
|
80
|
+
*/
|
|
81
|
+
function taintIsHeld(taint, runId) {
|
|
82
|
+
if (taint.runId === runId)
|
|
83
|
+
return true;
|
|
84
|
+
const owner = taint.pid;
|
|
85
|
+
if (owner === undefined || owner === _approvalsTaintDeps.pid())
|
|
86
|
+
return false;
|
|
87
|
+
return _approvalsTaintDeps.isProcessAlive(owner);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Start a trusted epoch. Entries that sat beside a taint are dropped, never
|
|
91
|
+
* promoted: nothing distinguishes a forged one from a human's.
|
|
92
|
+
*/
|
|
93
|
+
export async function clearApprovalsTaint(path, runId) {
|
|
94
|
+
return withPathFileLock(path, async () => {
|
|
95
|
+
const { taint } = await readApprovalsFile(path);
|
|
96
|
+
if (taint === undefined)
|
|
97
|
+
return "clean";
|
|
98
|
+
if (taintIsHeld(taint, runId))
|
|
99
|
+
return "held";
|
|
100
|
+
await writeApprovalsFile(path, { taint: undefined, entries: [] });
|
|
101
|
+
return "cleared";
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Taint (forge-capable) or clear (trusted) the store. Never throws: a failed
|
|
106
|
+
* clear leaves the taint in place, so the link abstains and the cost is
|
|
107
|
+
* prompts; a failed taint is logged, since a later run may then trust entries.
|
|
108
|
+
*/
|
|
109
|
+
export async function prepareApprovalsStore(opts) {
|
|
110
|
+
const logger = getSafeLogger();
|
|
111
|
+
try {
|
|
112
|
+
if (opts.forgeCapable) {
|
|
113
|
+
await taintApprovals(opts.approvalsFile, opts.runId);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const outcome = await clearApprovalsTaint(opts.approvalsFile, opts.runId);
|
|
117
|
+
if (outcome === "held") {
|
|
118
|
+
logger?.warn("permissions", "[approvals] store tainted by a forge-capable run still in progress; cache stays off", {
|
|
119
|
+
storyId: opts.storyId,
|
|
120
|
+
approvalsFile: opts.approvalsFile,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (outcome === "cleared") {
|
|
124
|
+
logger?.info("permissions", "[approvals] discarded entries written under a forge-capable run", {
|
|
125
|
+
storyId: opts.storyId,
|
|
126
|
+
approvalsFile: opts.approvalsFile,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger?.warn("permissions", "[approvals] could not update the store's taint marker", {
|
|
132
|
+
storyId: opts.storyId,
|
|
133
|
+
approvalsFile: opts.approvalsFile,
|
|
134
|
+
forgeCapable: opts.forgeCapable,
|
|
135
|
+
error,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ask-tier resolver chain (P2 design section 4).
|
|
3
|
+
*
|
|
4
|
+
* One decision point for every permission adjudicator: the approvals cache,
|
|
5
|
+
* a future classifier (P5) and the human gate are all LINKS here. The channel
|
|
6
|
+
* that asks a human lives in src/interaction/ and is not a peer of this chain
|
|
7
|
+
* -- it is what the human link talks to.
|
|
8
|
+
*
|
|
9
|
+
* Fail-closed by construction: the chain appends its own terminal deny, so an
|
|
10
|
+
* exhausted or all-abstaining chain denies whether or not the last link is
|
|
11
|
+
* total. A link that throws is treated as no answer, never as permission.
|
|
12
|
+
*/
|
|
13
|
+
import type { AskRequest } from "./types.ts";
|
|
14
|
+
/** A link's answer. `abstain` means "no opinion, try the next link". */
|
|
15
|
+
export type AskDecision = "allow" | "deny" | "abstain";
|
|
16
|
+
/** Who actually decided. Carried into the ledger so it never has to be inferred. */
|
|
17
|
+
export type AskDecidedBy = "cache" | "model" | "human" | "timeout" | "unavailable" | "cancelled" | "unshowable";
|
|
18
|
+
/**
|
|
19
|
+
* Per-ask control (US-003), carried SEPARATELY from the `AskRequest` so the
|
|
20
|
+
* request the ledger records stays the pure request and a resolver can react
|
|
21
|
+
* to the orchestrating turn being cancelled.
|
|
22
|
+
*/
|
|
23
|
+
export interface AskControl {
|
|
24
|
+
/** The turn's abort signal; aborted while a waiter sits on-screen or queued. */
|
|
25
|
+
readonly signal?: AbortSignal;
|
|
26
|
+
/** Notifies the turn loop that this ask is waiting on a human. */
|
|
27
|
+
readonly onWaiting?: () => void;
|
|
28
|
+
}
|
|
29
|
+
export interface AskLinkOutcome {
|
|
30
|
+
readonly decision: AskDecision;
|
|
31
|
+
readonly decidedBy: AskDecidedBy;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One adjudicator. A link ALWAYS names who decided, because a single link can
|
|
35
|
+
* answer for more than one reason -- the human link resolves as `human` when
|
|
36
|
+
* someone taps and as `timeout` when nobody does.
|
|
37
|
+
*/
|
|
38
|
+
export interface AskLink {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
resolve(req: AskRequest, control?: AskControl): Promise<AskLinkOutcome>;
|
|
41
|
+
}
|
|
42
|
+
/** What the runtime consumes. Never `abstain`. */
|
|
43
|
+
export interface AskVerdict {
|
|
44
|
+
readonly decision: "allow" | "deny";
|
|
45
|
+
readonly decidedBy: AskDecidedBy;
|
|
46
|
+
readonly latencyMs: number;
|
|
47
|
+
}
|
|
48
|
+
export interface AskResolver {
|
|
49
|
+
/**
|
|
50
|
+
* Whether a human can answer at all (ADR-030, amended for P4): an interaction
|
|
51
|
+
* channel exists for this run. Absent means false. Only the escalate Bash
|
|
52
|
+
* DESCRIPTION reads it; the verdict never depends on it.
|
|
53
|
+
*/
|
|
54
|
+
readonly humanReachable?: boolean;
|
|
55
|
+
resolve(req: AskRequest, control?: AskControl): Promise<AskVerdict>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compose links into a resolver. First non-abstain wins.
|
|
59
|
+
*
|
|
60
|
+
* A throwing link abstains rather than propagating: `runtime.callTool` turns an
|
|
61
|
+
* exception out of the resolver into a TOOL ERROR surfaced to the model, not a
|
|
62
|
+
* denial, which would lose the `denied:ask` ledger row and hand the agent
|
|
63
|
+
* something it may retry around.
|
|
64
|
+
*
|
|
65
|
+
* US-003: forwards the same `control` object to every link so a turn-cancel
|
|
66
|
+
* signal reaches whichever link would have answered, not just the first one
|
|
67
|
+
* that asks.
|
|
68
|
+
*/
|
|
69
|
+
export declare function chainAskLinks(links: readonly AskLink[]): AskResolver;
|