@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run an argv with no shell, a deadline, and a process-group kill.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from worktree/dependencies.ts so its two callers cannot drift.
|
|
5
|
+
* The three behaviours here were each a defect once and must not be rewritten
|
|
6
|
+
* from scratch: MEM-4 (a postinstall grandchild survived proc.kill() and kept
|
|
7
|
+
* running against a deleted worktree), BUG-13 (a hung install had no deadline),
|
|
8
|
+
* and the concurrent drain (a child that fills a pipe buffer never reaches
|
|
9
|
+
* `exited`, defeating the timeout).
|
|
10
|
+
*
|
|
11
|
+
* US-001 adds three more invariants on the same shape:
|
|
12
|
+
* - `signal?: AbortSignal` aborts the process group and reports `aborted`.
|
|
13
|
+
* - An already-aborted signal never spawns and resolves with exitCode -1.
|
|
14
|
+
* - After `exited`, both readers get `DRAIN_GRACE_MS` to close; if either
|
|
15
|
+
* stays open, the group is SIGKILLed and `orphansKilled: true` is set with
|
|
16
|
+
* whatever the readers captured so far.
|
|
17
|
+
*/
|
|
18
|
+
import { runtimeSpawn } from "#src/runtime/index";
|
|
19
|
+
import { killProcessGroup } from "./process-kill.js";
|
|
20
|
+
/**
|
|
21
|
+
* Time, in milliseconds, the readers are allowed to drain after `exited`
|
|
22
|
+
* before runArgv SIGKILLs the whole group and reports `orphansKilled`. US-001.
|
|
23
|
+
* Injectable for tests; not exposed as user configuration.
|
|
24
|
+
*/
|
|
25
|
+
export const DRAIN_GRACE_MS = 500;
|
|
26
|
+
/** Injectable seam, mirroring _worktreeDependencyDeps. */
|
|
27
|
+
export const _argvExecDeps = {
|
|
28
|
+
spawn: runtimeSpawn,
|
|
29
|
+
killProcessGroup,
|
|
30
|
+
/**
|
|
31
|
+
* The post-exit drain grace (US-001). A real test can shrink it to keep the
|
|
32
|
+
* "background process holding the pipe" case under a second; production
|
|
33
|
+
* stays at the DRAIN_GRACE_MS constant.
|
|
34
|
+
*/
|
|
35
|
+
drainGraceMs: DRAIN_GRACE_MS,
|
|
36
|
+
/**
|
|
37
|
+
* Injectable timer pair — lets the BUG-13 "hung install" test drive the
|
|
38
|
+
* timeout off a virtual clock instead of waiting the full 1s schema minimum.
|
|
39
|
+
* Mirrors `_heartbeatDeps` / `_idleWatchdogDeps` / `_authDeps`.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
setTimeout: ((fn, ms) => setTimeout(fn, ms)),
|
|
44
|
+
clearTimeout: ((id) => clearTimeout(id)),
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Build the child's env only when the caller actually asked for stripping or
|
|
48
|
+
* an overlay. Leaving it `undefined` otherwise means `Bun.spawn` inherits
|
|
49
|
+
* `process.env` on its own, which is both the cheaper path and what every
|
|
50
|
+
* caller without those options already relied on before this seam existed.
|
|
51
|
+
*/
|
|
52
|
+
function buildEnv(options) {
|
|
53
|
+
const hasStrip = (options.stripEnvVars?.length ?? 0) > 0;
|
|
54
|
+
const hasOverlay = options.env !== undefined && Object.keys(options.env).length > 0;
|
|
55
|
+
if (!hasStrip && !hasOverlay)
|
|
56
|
+
return undefined;
|
|
57
|
+
const env = { ...process.env };
|
|
58
|
+
for (const name of options.stripEnvVars ?? [])
|
|
59
|
+
delete env[name];
|
|
60
|
+
Object.assign(env, options.env ?? {});
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a ReadableStream<Uint8Array> to EOF, accumulating text. Resolves when
|
|
65
|
+
* the stream closes (done). The caller races this against a wall-clock
|
|
66
|
+
* deadline to detect an open-after-exit pipe (the `&`ed background case).
|
|
67
|
+
*
|
|
68
|
+
* `Response.text()` doesn't honour a deadline, so the reader is its own
|
|
69
|
+
* function (US-001 "Read stdout and stderr incrementally/concurrently").
|
|
70
|
+
*
|
|
71
|
+
* When `signal` aborts, the reader is cancelled so the pending `read()`
|
|
72
|
+
* resolves with `{done: true}`. The caller uses this to bound settlement
|
|
73
|
+
* after a process-group kill: a child that ignores the kill cannot pin
|
|
74
|
+
* runArgv forever on its still-open pipe.
|
|
75
|
+
*/
|
|
76
|
+
async function drainToEof(stream, signal) {
|
|
77
|
+
const reader = stream.getReader();
|
|
78
|
+
const decoder = new TextDecoder();
|
|
79
|
+
let text = "";
|
|
80
|
+
const onAbort = () => {
|
|
81
|
+
// Closing the stream forces the in-flight `read()` below to resolve with
|
|
82
|
+
// `{done: true}`; any bytes already captured stay in `text`.
|
|
83
|
+
reader.cancel().catch(() => {
|
|
84
|
+
// Cancelling after the pipe already closed is a no-op race; ignore.
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
try {
|
|
89
|
+
while (true) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done === true)
|
|
92
|
+
break;
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
text += decoder.decode(value, { stream: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Stream cancelled by SIGKILL on the process group; the partial text
|
|
99
|
+
// is still in `text`, so resolve with closed=false and let the caller
|
|
100
|
+
// decide.
|
|
101
|
+
}
|
|
102
|
+
text += decoder.decode();
|
|
103
|
+
if (signal !== undefined)
|
|
104
|
+
signal.removeEventListener("abort", onAbort);
|
|
105
|
+
try {
|
|
106
|
+
reader.releaseLock();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Already released; nothing to do.
|
|
110
|
+
}
|
|
111
|
+
return { text, closed: true };
|
|
112
|
+
}
|
|
113
|
+
export async function runArgv(options) {
|
|
114
|
+
const signal = options.signal;
|
|
115
|
+
// Already-aborted signal: never spawn. The caller gets `aborted: true` with
|
|
116
|
+
// a sentinel exitCode so a misread of the field can't masquerade as a real
|
|
117
|
+
// process completing (US-001 AC7).
|
|
118
|
+
if (signal?.aborted === true) {
|
|
119
|
+
return { exitCode: -1, stdout: "", stderr: "", timedOut: false, aborted: true, orphansKilled: false };
|
|
120
|
+
}
|
|
121
|
+
const env = buildEnv(options);
|
|
122
|
+
// nax-git-env-allow: generic argv executor; caller-supplied argv, not routed through gitSpawnEnv
|
|
123
|
+
const proc = _argvExecDeps.spawn([...options.argv], {
|
|
124
|
+
cwd: options.cwd,
|
|
125
|
+
stdout: "pipe",
|
|
126
|
+
stderr: "pipe",
|
|
127
|
+
...(env !== undefined ? { env } : {}),
|
|
128
|
+
// MEM-4: setsid() makes this pid the process-group id, so a group kill
|
|
129
|
+
// reaches grandchildren (a package manager's postinstall) rather than only
|
|
130
|
+
// the direct child.
|
|
131
|
+
detached: true,
|
|
132
|
+
});
|
|
133
|
+
let timedOut = false;
|
|
134
|
+
let aborted = false;
|
|
135
|
+
let orphansKilled = false;
|
|
136
|
+
const killGroup = () => {
|
|
137
|
+
_argvExecDeps.killProcessGroup(proc.pid, "SIGKILL");
|
|
138
|
+
};
|
|
139
|
+
const stdoutController = new AbortController();
|
|
140
|
+
const stderrController = new AbortController();
|
|
141
|
+
const stopReaders = () => {
|
|
142
|
+
stdoutController.abort();
|
|
143
|
+
stderrController.abort();
|
|
144
|
+
};
|
|
145
|
+
// BUG-13: unlike every git call (routed through gitWithTimeout), a spawn
|
|
146
|
+
// with no deadline can block its caller forever on a hung install
|
|
147
|
+
// (registry/NFS stall).
|
|
148
|
+
const timerId = _argvExecDeps.setTimeout(() => {
|
|
149
|
+
timedOut = true;
|
|
150
|
+
// MEM-4: proc.kill() reaches only the direct child, orphaning postinstall
|
|
151
|
+
// grandchildren. killProcessGroup(pid, "SIGKILL") kills the whole group
|
|
152
|
+
// (negative pid), falling back to the single process on ESRCH.
|
|
153
|
+
killGroup();
|
|
154
|
+
stopReaders();
|
|
155
|
+
}, options.timeoutMs);
|
|
156
|
+
// US-001: an abort races the timeout. One listener, removed on every settle
|
|
157
|
+
// path so a never-aborted signal never leaks a registration (AC10).
|
|
158
|
+
const onAbort = () => {
|
|
159
|
+
aborted = true;
|
|
160
|
+
killGroup();
|
|
161
|
+
stopReaders();
|
|
162
|
+
};
|
|
163
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
// Read stdout/stderr concurrently with the exit wait — a process that
|
|
165
|
+
// fills a pipe's OS buffer before being read would otherwise block on the
|
|
166
|
+
// write and never reach `exited`, defeating the timeout's own SIGKILL.
|
|
167
|
+
// Each reader has its own controller so a still-pending one can be forced
|
|
168
|
+
// to settle (via reader.cancel()) after we kill the process group. US-001
|
|
169
|
+
// requires that settlement stay bounded even when a background process
|
|
170
|
+
// inherits the pipe and ignores the SIGKILL.
|
|
171
|
+
const stdoutPromise = drainToEof(proc.stdout, stdoutController.signal);
|
|
172
|
+
const stderrPromise = drainToEof(proc.stderr, stderrController.signal);
|
|
173
|
+
const exitCode = await proc.exited;
|
|
174
|
+
// Post-exit drain grace. After exit, the streams MUST close — they were
|
|
175
|
+
// owned by the shell, which is now dead. Background processes that
|
|
176
|
+
// inherited the pipe (the `&` case) keep it open: that's the orphan.
|
|
177
|
+
const graceMs = _argvExecDeps.drainGraceMs;
|
|
178
|
+
// Cleared on every settle path: a successful command whose readers close
|
|
179
|
+
// before the grace elapses would otherwise leave a pending timer whose
|
|
180
|
+
// callback fires 500ms later and resolves an orphaned promise. Under load
|
|
181
|
+
// the leaked registrations accumulate in the timer wheel for nothing.
|
|
182
|
+
let graceTimerId;
|
|
183
|
+
const gracePromise = new Promise((resolve) => {
|
|
184
|
+
graceTimerId = _argvExecDeps.setTimeout(() => resolve("expired"), graceMs);
|
|
185
|
+
});
|
|
186
|
+
const stdoutSettled = await Promise.race([
|
|
187
|
+
stdoutPromise,
|
|
188
|
+
gracePromise.then(() => "expired"),
|
|
189
|
+
]);
|
|
190
|
+
const stderrSettled = await Promise.race([
|
|
191
|
+
stderrPromise,
|
|
192
|
+
gracePromise.then(() => "expired"),
|
|
193
|
+
]);
|
|
194
|
+
if (graceTimerId !== undefined)
|
|
195
|
+
_argvExecDeps.clearTimeout(graceTimerId);
|
|
196
|
+
const stdoutClosed = stdoutSettled !== "expired";
|
|
197
|
+
const stderrClosed = stderrSettled !== "expired";
|
|
198
|
+
if (!stdoutClosed || !stderrClosed) {
|
|
199
|
+
orphansKilled = true;
|
|
200
|
+
killGroup();
|
|
201
|
+
// The OS should close the pipes once the group dies; race that against
|
|
202
|
+
// a small post-kill budget. If a holdout somehow ignores SIGKILL or is
|
|
203
|
+
// not yet reaped, abort the matching reader controller — that drives
|
|
204
|
+
// `reader.cancel()` in drainToEof, which forces the pending `read()` to
|
|
205
|
+
// resolve with `done: true` and bounds settlement.
|
|
206
|
+
if (!stdoutClosed)
|
|
207
|
+
stdoutController.abort();
|
|
208
|
+
if (!stderrClosed)
|
|
209
|
+
stderrController.abort();
|
|
210
|
+
}
|
|
211
|
+
// Always await the readers to harvest whatever they captured.
|
|
212
|
+
const stdoutFinal = stdoutClosed ? stdoutSettled : await stdoutPromise;
|
|
213
|
+
const stderrFinal = stderrClosed ? stderrSettled : await stderrPromise;
|
|
214
|
+
_argvExecDeps.clearTimeout(timerId);
|
|
215
|
+
signal?.removeEventListener("abort", onAbort);
|
|
216
|
+
return {
|
|
217
|
+
exitCode,
|
|
218
|
+
stdout: stdoutFinal.text,
|
|
219
|
+
stderr: stderrFinal.text,
|
|
220
|
+
timedOut,
|
|
221
|
+
aborted,
|
|
222
|
+
orphansKilled,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceilings that bound the WORK, not just the answer.
|
|
3
|
+
*
|
|
4
|
+
* Every tool honoured `maxBytes` on the way out, but produced its output by
|
|
5
|
+
* reading a whole file or draining a whole stream first. The ceiling described
|
|
6
|
+
* what the model was told while the memory it took to get there was unbounded,
|
|
7
|
+
* so a large but entirely permitted in-root file was enough to exhaust it --
|
|
8
|
+
* no traversal, no escape, nothing the permission policy could see.
|
|
9
|
+
*
|
|
10
|
+
* Both helpers deliberately return up to `maxBytes + 1` bytes. That extra byte
|
|
11
|
+
* is what lets a caller distinguish "this is the whole thing" from "there was
|
|
12
|
+
* more", without a second stat or read.
|
|
13
|
+
*/
|
|
14
|
+
/** Read at most `maxBytes + 1` bytes of a UTF-8 file, never the whole file. */
|
|
15
|
+
export declare function readPrefix(path: string, maxBytes: number): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Consume a stream until `maxBytes + 1` bytes have arrived, then stop.
|
|
18
|
+
*
|
|
19
|
+
* Cancelling the reader is the point: a subprocess whose output is no longer
|
|
20
|
+
* being consumed blocks on its own pipe and is then reaped by its caller's
|
|
21
|
+
* timeout, instead of being allowed to fill memory for as long as that timeout
|
|
22
|
+
* permits. `git log -p` on a large repository is the case in mind.
|
|
23
|
+
*/
|
|
24
|
+
export declare function drainBounded(stream: ReadableStream<Uint8Array>, maxBytes: number): Promise<string>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceilings that bound the WORK, not just the answer.
|
|
3
|
+
*
|
|
4
|
+
* Every tool honoured `maxBytes` on the way out, but produced its output by
|
|
5
|
+
* reading a whole file or draining a whole stream first. The ceiling described
|
|
6
|
+
* what the model was told while the memory it took to get there was unbounded,
|
|
7
|
+
* so a large but entirely permitted in-root file was enough to exhaust it --
|
|
8
|
+
* no traversal, no escape, nothing the permission policy could see.
|
|
9
|
+
*
|
|
10
|
+
* Both helpers deliberately return up to `maxBytes + 1` bytes. That extra byte
|
|
11
|
+
* is what lets a caller distinguish "this is the whole thing" from "there was
|
|
12
|
+
* more", without a second stat or read.
|
|
13
|
+
*/
|
|
14
|
+
import { open } from "node:fs/promises";
|
|
15
|
+
/** Read at most `maxBytes + 1` bytes of a UTF-8 file, never the whole file. */
|
|
16
|
+
export async function readPrefix(path, maxBytes) {
|
|
17
|
+
const handle = await open(path, "r");
|
|
18
|
+
try {
|
|
19
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
20
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
21
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await handle.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Consume a stream until `maxBytes + 1` bytes have arrived, then stop.
|
|
29
|
+
*
|
|
30
|
+
* Cancelling the reader is the point: a subprocess whose output is no longer
|
|
31
|
+
* being consumed blocks on its own pipe and is then reaped by its caller's
|
|
32
|
+
* timeout, instead of being allowed to fill memory for as long as that timeout
|
|
33
|
+
* permits. `git log -p` on a large repository is the case in mind.
|
|
34
|
+
*/
|
|
35
|
+
export async function drainBounded(stream, maxBytes) {
|
|
36
|
+
const reader = stream.getReader();
|
|
37
|
+
const decoder = new TextDecoder();
|
|
38
|
+
let out = "";
|
|
39
|
+
try {
|
|
40
|
+
while (Buffer.byteLength(out, "utf8") <= maxBytes) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done)
|
|
43
|
+
break;
|
|
44
|
+
out += decoder.decode(value, { stream: true });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await reader.cancel().catch(() => { });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A declared quality command. A plain string is one shell command (the
|
|
3
|
+
* historical shape). A list means "run every entry, report every failure" —
|
|
4
|
+
* the form that exists because `a && b` short-circuits, hiding each failure
|
|
5
|
+
* after the first from an agent that pays a full round trip per invocation
|
|
6
|
+
* (nax#1990).
|
|
7
|
+
*/
|
|
8
|
+
export type QualityCommandSpec = string | string[];
|
|
9
|
+
/**
|
|
10
|
+
* Flatten a spec to the list of commands to actually run. Blank entries are
|
|
11
|
+
* dropped so a stray "" in a list cannot spawn an empty shell; a spec that is
|
|
12
|
+
* entirely blank returns [], which callers treat as "not declared".
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeCommandSpec(spec: QualityCommandSpec | undefined): string[];
|
|
15
|
+
/**
|
|
16
|
+
* True when any entry chains with `&&`. Used only to warn: a chain still runs
|
|
17
|
+
* exactly as it always did, it just hides later failures.
|
|
18
|
+
*/
|
|
19
|
+
export declare function containsShellChain(spec: QualityCommandSpec | undefined): boolean;
|
|
20
|
+
/** True when any command entry includes the supplied literal fragment. */
|
|
21
|
+
export declare function commandSpecIncludes(spec: QualityCommandSpec | undefined, fragment: string): boolean;
|
|
22
|
+
/** Apply a literal replacement to every command entry without changing its shape. */
|
|
23
|
+
export declare function replaceInCommandSpec(spec: QualityCommandSpec, searchValue: string, replacement: string): QualityCommandSpec;
|
|
24
|
+
/**
|
|
25
|
+
* Render a spec as the single string that prompt and context surfaces show a
|
|
26
|
+
* reader. `" && "` is what a human recognises and would type, even though a
|
|
27
|
+
* list does not actually short-circuit when it runs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderCommandSpec(spec: QualityCommandSpec | undefined): string | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten a spec to the list of commands to actually run. Blank entries are
|
|
3
|
+
* dropped so a stray "" in a list cannot spawn an empty shell; a spec that is
|
|
4
|
+
* entirely blank returns [], which callers treat as "not declared".
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeCommandSpec(spec) {
|
|
7
|
+
if (spec === undefined)
|
|
8
|
+
return [];
|
|
9
|
+
const entries = typeof spec === "string" ? [spec] : spec;
|
|
10
|
+
return entries.map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* True when any entry chains with `&&`. Used only to warn: a chain still runs
|
|
14
|
+
* exactly as it always did, it just hides later failures.
|
|
15
|
+
*/
|
|
16
|
+
export function containsShellChain(spec) {
|
|
17
|
+
return normalizeCommandSpec(spec).some((entry) => entry.includes("&&"));
|
|
18
|
+
}
|
|
19
|
+
/** True when any command entry includes the supplied literal fragment. */
|
|
20
|
+
export function commandSpecIncludes(spec, fragment) {
|
|
21
|
+
return normalizeCommandSpec(spec).some((entry) => entry.includes(fragment));
|
|
22
|
+
}
|
|
23
|
+
/** Apply a literal replacement to every command entry without changing its shape. */
|
|
24
|
+
export function replaceInCommandSpec(spec, searchValue, replacement) {
|
|
25
|
+
const replace = (command) => command.replaceAll(searchValue, replacement);
|
|
26
|
+
return typeof spec === "string" ? replace(spec) : spec.map(replace);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Render a spec as the single string that prompt and context surfaces show a
|
|
30
|
+
* reader. `" && "` is what a human recognises and would type, even though a
|
|
31
|
+
* list does not actually short-circuit when it runs.
|
|
32
|
+
*/
|
|
33
|
+
export function renderCommandSpec(spec) {
|
|
34
|
+
const steps = normalizeCommandSpec(spec);
|
|
35
|
+
return steps.length === 0 ? undefined : steps.join(" && ");
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary shared by the tool-input validator (`src/agents/native/session/
|
|
3
|
+
* tool-input-validation.ts`, nax#2047) and `run-command.ts`'s own `values`
|
|
4
|
+
* shape guard (#1924): the "actual" half of a violation reads exactly the
|
|
5
|
+
* way run-command.ts itself would have phrased the same shape, so the gate
|
|
6
|
+
* and the tool produce the same wording.
|
|
7
|
+
*/
|
|
8
|
+
export declare function describeValuesType(value: unknown): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary shared by the tool-input validator (`src/agents/native/session/
|
|
3
|
+
* tool-input-validation.ts`, nax#2047) and `run-command.ts`'s own `values`
|
|
4
|
+
* shape guard (#1924): the "actual" half of a violation reads exactly the
|
|
5
|
+
* way run-command.ts itself would have phrased the same shape, so the gate
|
|
6
|
+
* and the tool produce the same wording.
|
|
7
|
+
*/
|
|
8
|
+
export function describeValuesType(value) {
|
|
9
|
+
if (value === null)
|
|
10
|
+
return "null";
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return "an array";
|
|
13
|
+
return `a ${typeof value}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame a finished spawn result the way the model expects to see it: an
|
|
3
|
+
* aborted call opens with the cancellation line, an orphansKilled call appends
|
|
4
|
+
* the `[nax]` footer, a timed-out call reports the deadline, and everything
|
|
5
|
+
* else gets `exit N\nstdout\nstderr` (US-001 AC14/AC15).
|
|
6
|
+
*
|
|
7
|
+
* One function shared by the `Bash` tool and `RunCommand` (Exec) so the two
|
|
8
|
+
* ladders a model reads interchangeably cannot drift — the deadline label is
|
|
9
|
+
* the only difference between the call sites, so it is a parameter.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatExecBody(launched: {
|
|
12
|
+
aborted?: boolean;
|
|
13
|
+
orphansKilled?: boolean;
|
|
14
|
+
timedOut: boolean;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}, timeoutMs: number): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame a finished spawn result the way the model expects to see it: an
|
|
3
|
+
* aborted call opens with the cancellation line, an orphansKilled call appends
|
|
4
|
+
* the `[nax]` footer, a timed-out call reports the deadline, and everything
|
|
5
|
+
* else gets `exit N\nstdout\nstderr` (US-001 AC14/AC15).
|
|
6
|
+
*
|
|
7
|
+
* One function shared by the `Bash` tool and `RunCommand` (Exec) so the two
|
|
8
|
+
* ladders a model reads interchangeably cannot drift — the deadline label is
|
|
9
|
+
* the only difference between the call sites, so it is a parameter.
|
|
10
|
+
*/
|
|
11
|
+
export function formatExecBody(launched, timeoutMs) {
|
|
12
|
+
if (launched.aborted === true) {
|
|
13
|
+
return `Cancelled: the turn ended while this command was running.\nexit ${launched.exitCode}\n${launched.stdout}\n${launched.stderr}`;
|
|
14
|
+
}
|
|
15
|
+
if (launched.orphansKilled === true) {
|
|
16
|
+
return `exit ${launched.exitCode}\n${launched.stdout}\n${launched.stderr}\n[nax] background processes still holding the output were killed`;
|
|
17
|
+
}
|
|
18
|
+
if (launched.timedOut) {
|
|
19
|
+
return `timed out after ${timeoutMs}ms`;
|
|
20
|
+
}
|
|
21
|
+
return `exit ${launched.exitCode}\n${launched.stdout}\n${launched.stderr}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared single-file lock used by `withPathFileLock` and `withQueueFileLock`.
|
|
3
|
+
*
|
|
4
|
+
* Mutual exclusion derives from the atomicity of an exclusive create: the
|
|
5
|
+
* lock is ONE file at `<target>.lock`, and the writer whose
|
|
6
|
+
* `writeFile(path, pid, { flag: "wx" })` succeeds holds the lock. There is
|
|
7
|
+
* no candidate ordering, no filesystem birthtime, and no polling race.
|
|
8
|
+
*
|
|
9
|
+
* #1731: the previous design created timestamped candidate files and polled
|
|
10
|
+
* until it saw itself as the "oldest live" candidate. That protocol is only
|
|
11
|
+
* sound if candidate order matches creation order, which it did not: the
|
|
12
|
+
* sort key was `(stat().birthtimeMs, name.localeCompare)` — on Linux CI
|
|
13
|
+
* filesystems birthtimeMs is degenerate so order collapsed to the random
|
|
14
|
+
* uuid tail of the name, and a poll whose readdir completed before a
|
|
15
|
+
* competitor's create landed missed that competitor entirely. A
|
|
16
|
+
* later-created candidate could therefore sort before the holder, also
|
|
17
|
+
* conclude it was oldest, and enter — two writers inside the critical
|
|
18
|
+
* section, one write silently lost. An exclusive create cannot exhibit any
|
|
19
|
+
* of those failure modes: exactly one create ever succeeds.
|
|
20
|
+
*
|
|
21
|
+
* Staleness (BUG-10 / BUG-25, carried over from the candidate design):
|
|
22
|
+
* - The holder's pid is stored in the lock file. A waiter that loses the
|
|
23
|
+
* create race reads the pid; a PROVEN-dead holder (ESRCH) means the lock
|
|
24
|
+
* is abandoned and may be reclaimed.
|
|
25
|
+
* - Content that cannot be parsed (e.g. the creator crashed between the
|
|
26
|
+
* create and the pid write) is respected until it ages past
|
|
27
|
+
* `EMPTY_LOCK_EVICT_AGE_MS` — a live holder writes its pid within the
|
|
28
|
+
* same tick as the create, so old unparseable content proves abandonment.
|
|
29
|
+
* Inconclusive liveness fails closed: the waiter times out and throws
|
|
30
|
+
* rather than entering over a possible holder.
|
|
31
|
+
*
|
|
32
|
+
* Reclamation is serialized by a claim file (`<target>.lock.claim`), whose
|
|
33
|
+
* exclusive create elects a single "gravedigger" at a time. This closes the
|
|
34
|
+
* steal race: without it, two waiters that both observed the dead holder
|
|
35
|
+
* could interleave (stealer A's create landing between stealer B's
|
|
36
|
+
* re-verification and its unlink), letting B unlink A's fresh live lock and
|
|
37
|
+
* enter over it. Under the claim:
|
|
38
|
+
* - Only the claim holder may unlink the lock file, so no unlink can make
|
|
39
|
+
* the path absent during another gravedigger's verify→unlink window.
|
|
40
|
+
* - The gravedigger RE-VERIFIES abandonment after winning the claim (a
|
|
41
|
+
* stale observation must not kill a lock that a new live holder has
|
|
42
|
+
* since created) and only then unlinks.
|
|
43
|
+
* - If two gravediggers ever do overlap (only possible via the claim's own
|
|
44
|
+
* age-based steal), both re-verify the same dead file, both unlink (the
|
|
45
|
+
* second gets ENOENT), and the exclusive create on the fixed lock path
|
|
46
|
+
* still admits exactly one holder.
|
|
47
|
+
* The claim itself is held for microseconds, so it is stolen purely on age
|
|
48
|
+
* (`CLAIM_MAX_AGE_MS`): a claim older than that proves its gravedigger died
|
|
49
|
+
* mid-reclaim. A crashed gravedigger otherwise pauses reclamation until
|
|
50
|
+
* then — fail closed, never fail into someone else's live lock.
|
|
51
|
+
*
|
|
52
|
+
* Release unlinks the lock file in the `finally` block. The unlink is
|
|
53
|
+
* swallowed because release runs from the same process that created the
|
|
54
|
+
* file — a double-cleanup on retry paths must not throw. A crashed holder
|
|
55
|
+
* leaks the file, which the dead-pid reclamation above reclaims.
|
|
56
|
+
*
|
|
57
|
+
* Mixed versions: the pre-#1731 design wrote `<target>.lock.<time>.<pid>.<uuid>`
|
|
58
|
+
* candidate files, which this design deliberately does NOT scan for — they
|
|
59
|
+
* are inert leftovers here (and legacy candidate files are never GC'd). An
|
|
60
|
+
* old-version process and a new-version process must therefore not run
|
|
61
|
+
* concurrently against the same target during a one-release transition
|
|
62
|
+
* window: each would see only its own lock format and could enter
|
|
63
|
+
* concurrently. This self-heals once old-version processes exit.
|
|
64
|
+
*/
|
|
65
|
+
import { readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
66
|
+
import { isProcessAlive } from "./process-alive.ts";
|
|
67
|
+
export interface FileLockOptions {
|
|
68
|
+
/** Milliseconds between acquisition retries. Default 10. */
|
|
69
|
+
retryMs?: number;
|
|
70
|
+
/** Maximum time to wait for the lock before throwing. Default 5000. */
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
/** Lock family used in the timeout error, e.g. "path" or "queue". */
|
|
73
|
+
lockName: string;
|
|
74
|
+
/** Log/error prefix, e.g. "[path-lock]". */
|
|
75
|
+
errorPrefix: string;
|
|
76
|
+
}
|
|
77
|
+
export declare const _fileLockDeps: {
|
|
78
|
+
writeFile: typeof writeFile;
|
|
79
|
+
readFile: typeof readFile;
|
|
80
|
+
stat: typeof stat;
|
|
81
|
+
unlink: typeof unlink;
|
|
82
|
+
now: () => number;
|
|
83
|
+
sleep: (ms: number) => Promise<void>;
|
|
84
|
+
isPidAlive: typeof isProcessAlive;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Serialize `operation` against any other process holding the lock at
|
|
88
|
+
* `lockPath`. See the module doc for the acquisition, staleness, and
|
|
89
|
+
* release semantics.
|
|
90
|
+
*/
|
|
91
|
+
export declare function withFileLock<T>(lockPath: string, operation: () => Promise<T>, options: FileLockOptions): Promise<T>;
|