@sema-agent/server 1.323.0 → 2.0.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/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +58 -2
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RemoteHostExecutionEnv` — the **TOC `host` exec adapter** (DUAL-MODE-DESIGN §5). Runs
|
|
3
|
+
* commands DIRECTLY on THIS machine, in NO container — the TOC ("take-over-this-computer") default, exactly
|
|
4
|
+
* like a local coding agent operating on the user's own box. **Isolation = none.**
|
|
5
|
+
*
|
|
6
|
+
* It is a PEER adapter to the E2B / SSH / k8s ones (not chained): all four implement the SAME core seam
|
|
7
|
+
* `RemoteExecutionEnv` (core src/core/remote-env.ts, design/48 §5) so the agent's "hand" plugs into any of
|
|
8
|
+
* them via the deployment-level `ExecutionEnvFactory`. The discriminator that selects between them is the
|
|
9
|
+
* `remoteExec.provider` superset — this adapter is `provider:"host"` (DUAL-MODE-DESIGN §5).
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **`host` targets the operator's OWN machine, not a disposable sandbox** (DUAL-MODE-DESIGN §5):
|
|
12
|
+
* - `capabilities = { isolation: false, suspendable: false }` — there is NO container boundary and NO
|
|
13
|
+
* snapshot. Side effects are PERMANENT and visible to the whole machine. The orchestrator/leader MUST
|
|
14
|
+
* tighten autonomy + the design/37 policy gate + HITL on a non-isolated env (design/53 zero-trust) —
|
|
15
|
+
* identical posture to the SSH adapter, which also runs on a real (un-isolated) system.
|
|
16
|
+
* - **NO network restriction** — this is the user's own machine; the agent has the same network the user
|
|
17
|
+
* does (that is the whole point of the TOC mode).
|
|
18
|
+
* - `suspendVM`/`resumeVM`/`postResumeInit` → typed `"unsupported"` (a live host process tree isn't
|
|
19
|
+
* snapshotable — durable suspend N/A). `reconnect` is a no-op success (the "connection" is the local
|
|
20
|
+
* process; nothing to re-establish).
|
|
21
|
+
* - `destroy()` / `cleanup()` **`rm -rf` the per-task working directory** (the workspace this adapter
|
|
22
|
+
* created), NOT the machine. Anything the agent created OUTSIDE the workspace persists (that is the
|
|
23
|
+
* operator's/gate's concern — there is no isolation to undo).
|
|
24
|
+
*
|
|
25
|
+
* **never-throw invariant:** every base `ExecutionEnv` FileSystem/Shell method encodes failure in its
|
|
26
|
+
* returned `Result` (never throws). `execStream`'s iterator MAY throw `RemoteExecutionError` mid-stream
|
|
27
|
+
* before an `exit` chunk (the seam contract, OutputChunk). A non-zero command exit is a NORMAL value
|
|
28
|
+
* (`{ ok:true, exitCode }`), never an error.
|
|
29
|
+
*
|
|
30
|
+
* Platforms: macOS + Linux (POSIX shell). Forward-compatible with the `remoteExec` superset: this file also
|
|
31
|
+
* scaffolds the `local-docker` / `remote-docker` providers' factory entry points (typed stubs that throw a
|
|
32
|
+
* clear "not yet implemented" — explicit fast-follow per DUAL-MODE-DESIGN §5; NOT faked green).
|
|
33
|
+
*/
|
|
1
34
|
import { spawn } from "node:child_process";
|
|
2
35
|
import os from "node:os";
|
|
3
36
|
import path from "node:path";
|
|
@@ -6,24 +39,37 @@ import { randomBytes } from "node:crypto";
|
|
|
6
39
|
import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync } from "node:fs";
|
|
7
40
|
import { StringDecoder } from "node:string_decoder";
|
|
8
41
|
import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
|
|
42
|
+
// design/158 B4 (lens4 N8): the two host-lane kill switches were the negative names `HOST_BG_DISABLED` /
|
|
43
|
+
// `HOST_EXEC_SPOOL_DISABLED`, parsed inline. Their canonical names are now positive (`HOST_BG_ENABLED` /
|
|
44
|
+
// `HOST_EXEC_SPOOL_ENABLED`, default ON) and resolved by the shared config helpers, which keep the retired
|
|
45
|
+
// names working for one deprecation window and register both in the boot polarity table.
|
|
9
46
|
import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
10
47
|
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
|
|
11
48
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
12
49
|
import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
13
50
|
const PROVIDER = "host";
|
|
51
|
+
/** Default per-command wall-clock when the caller passes none (NOT a hang detector — host commands rarely hang). */
|
|
14
52
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
53
|
+
// ─── design/103 host-lane background shell: TOC's run_in_background / BashOutput / KillShell ───
|
|
54
|
+
// host driver = design/103 §5.2 path ①: a `detached` child (its OWN process group via `detached:true` → pgid ===
|
|
55
|
+
// child.pid) writes stdout/stderr DIRECTLY to two local files; poll re-reads each by byte cursor; kill/dispose reap
|
|
56
|
+
// the whole group with `process.kill(-pid)`. The BackgroundShellManager owns every red-line (opaque id / owned-id
|
|
57
|
+
// isolation / maxConcurrent / timeout hard wall / dispose); this driver only writes the 4 transport primitives.
|
|
15
58
|
const HOST_BG_MAX_CONCURRENT = numEnvOr("HOST_BG_MAX_CONCURRENT", 8, 1);
|
|
16
59
|
const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
|
|
17
60
|
const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
|
|
18
|
-
const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024);
|
|
19
|
-
const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP);
|
|
20
|
-
const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
|
|
61
|
+
const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024); // per-poll incremental read ceiling
|
|
62
|
+
const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP); // disk guard: reap a runaway
|
|
63
|
+
const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0); // SIGTERM → grace → SIGKILL (no host isolation)
|
|
64
|
+
/** Per-stream memory-mode drain cap: an un-polled adopted child cannot grow the worker heap without bound (the
|
|
65
|
+
* host lane has no VM to contain it). Drop-oldest beyond this, counting dropped bytes → surfaced as `truncated`. */
|
|
21
66
|
const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
|
|
22
67
|
const ok = (value) => ({ ok: true, value });
|
|
23
68
|
const unsupported = (op) => ({
|
|
24
69
|
ok: false,
|
|
25
70
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a host target (the operator's own machine — no container boundary, not snapshotable; capabilities.suspendable=false)`),
|
|
26
71
|
});
|
|
72
|
+
// The cap shape an env without a backend advertises — `supported:false` makes `hasScheduler` false (INERT, no tools).
|
|
27
73
|
const UNSUPPORTED_SCHEDULER_CAPS = {
|
|
28
74
|
supported: false,
|
|
29
75
|
maxScheduledPerScope: 0,
|
|
@@ -32,7 +78,11 @@ const UNSUPPORTED_SCHEDULER_CAPS = {
|
|
|
32
78
|
maxScheduledHorizonSec: 0,
|
|
33
79
|
};
|
|
34
80
|
export class RemoteHostExecutionEnv {
|
|
81
|
+
/** design/61 §9 / DUAL-MODE-DESIGN §5: a `host` env is the operator's real machine — no isolation, no snapshot. */
|
|
35
82
|
capabilities = { isolation: false, suspendable: false };
|
|
83
|
+
/** R7 自唤醒: structural delegation to the injected scheduler backend. `hasScheduler(env)` checks BOTH the method
|
|
84
|
+
* presence (always, since we implement the interface) AND `schedulerCapabilities.supported===true` — which is only
|
|
85
|
+
* true when a backend was injected, so a host env without one stays INERT (the four scheduler tools don't mount). */
|
|
36
86
|
get schedulerCapabilities() {
|
|
37
87
|
return this.cfg.scheduler?.schedulerCapabilities ?? UNSUPPORTED_SCHEDULER_CAPS;
|
|
38
88
|
}
|
|
@@ -51,30 +101,45 @@ export class RemoteHostExecutionEnv {
|
|
|
51
101
|
? this.cfg.scheduler.list(ctx)
|
|
52
102
|
: Promise.resolve({ ok: false, error: new SchedulerError("unsupported", "scheduler not enabled on this worker") });
|
|
53
103
|
}
|
|
104
|
+
/** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at the workspace root. */
|
|
54
105
|
cwd;
|
|
55
106
|
workspaceDir;
|
|
56
107
|
cfg;
|
|
108
|
+
/** Workspace created on disk (lazily, on first use). */
|
|
57
109
|
prepared = false;
|
|
110
|
+
/** Terminal once {@link destroy} ran — guards fs/exec from running against a removed workspace. */
|
|
58
111
|
destroyed = false;
|
|
112
|
+
/** [1116] 五轮复审:fg/bg spool 根=tmpdir 私有目录(core bgSpoolDir 同形)。workspace 是命令可写区,
|
|
113
|
+
* `git clean -fdx` 类清理会 unlink 活跃 spool(子进程经继承 fd 对 unlinked inode 继续写,而父进程
|
|
114
|
+
* 按路径 statSync/read 静默断=输出丢失+轮转帽失效);顺带不再弄脏 user-owned workspace(TOC/CC
|
|
115
|
+
* persistent 形)。懒建;destroy 整树清。 */
|
|
59
116
|
spoolRootDir;
|
|
117
|
+
/** 🔴 在飞的 fg spool 目录登记表(2026-07-25)。`destroy()` 原来只看 `bgManager.hasShells()` 就 `rm -rf`
|
|
118
|
+
* spool 根 —— 而**前台** exec 的 spool 也在同一个根下,子进程通过继承的 fd 继续往已 unlink 的 inode 写,
|
|
119
|
+
* 父进程按**路径** stat/read 于是什么都看不到 ⇒ 命令照跑、副作用照落盘,但 stdout 被静默截断,还返回
|
|
120
|
+
* `exitCode 0`。对模型而言那与「成功且无输出」**不可区分**。(与 core RB-73 同形:写已释放的 inode。)
|
|
121
|
+
* 判据与 `hasShells()` 并列:**只要还有人在读写这个根下的文件,就不删它**。 */
|
|
60
122
|
liveFgSpools = new Set();
|
|
61
123
|
ensureSpoolRoot() {
|
|
62
124
|
if (!this.spoolRootDir)
|
|
63
125
|
this.spoolRootDir = mkdtempSync(path.join(os.tmpdir(), "sema-host-spool-"));
|
|
64
126
|
return this.spoolRootDir;
|
|
65
127
|
}
|
|
128
|
+
/** True when `workspaceDir` was given (user-owned persistent dir) → destroy() must NOT delete it. */
|
|
66
129
|
persistent;
|
|
130
|
+
/** Memoized lazy `mkdir -p` so concurrent first-use prepares the workspace exactly once. */
|
|
67
131
|
preparing;
|
|
68
132
|
handle;
|
|
69
133
|
constructor(config = {}) {
|
|
70
134
|
if (config.workspaceDir) {
|
|
135
|
+
// TOC/CC mode: the user's OWN dir, verbatim — no random subdir, never deleted (see destroy()).
|
|
71
136
|
this.workspaceDir = path.resolve(config.workspaceDir);
|
|
72
137
|
this.persistent = true;
|
|
73
138
|
}
|
|
74
139
|
else {
|
|
75
140
|
const base = config.workspaceBase ?? os.tmpdir();
|
|
76
141
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
77
|
-
const suffix = randomBytes(4).toString("hex");
|
|
142
|
+
const suffix = randomBytes(4).toString("hex"); // always-unique tail so a reused id never collides
|
|
78
143
|
this.workspaceDir = path.resolve(base, `sema-host-${sanitizeId(id)}-${suffix}`);
|
|
79
144
|
this.persistent = false;
|
|
80
145
|
}
|
|
@@ -85,12 +150,13 @@ export class RemoteHostExecutionEnv {
|
|
|
85
150
|
this.cwd = this.workspaceDir;
|
|
86
151
|
this.handle = { sandboxId: this.workspaceDir, provider: PROVIDER, mountPath: this.workspaceDir };
|
|
87
152
|
}
|
|
153
|
+
// ─────────────────────────────── lifecycle ───────────────────────────────
|
|
88
154
|
workspaceHandle() {
|
|
89
155
|
return this.handle;
|
|
90
156
|
}
|
|
91
157
|
async connect(config) {
|
|
92
158
|
if (config?.snapshotId)
|
|
93
|
-
return unsupported("connect(snapshotId)");
|
|
159
|
+
return unsupported("connect(snapshotId)"); // host has no snapshots
|
|
94
160
|
if (this.destroyed)
|
|
95
161
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
96
162
|
if (config?.abortSignal?.aborted)
|
|
@@ -102,6 +168,7 @@ export class RemoteHostExecutionEnv {
|
|
|
102
168
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `failed to create host workspace ${this.workspaceDir}: ${errMsg(e)}`, asError(e)) };
|
|
103
169
|
}
|
|
104
170
|
if (config?.secrets?.length) {
|
|
171
|
+
// Secrets are injected per-command via cfg.env (resolved by the control plane), not at connect — record only.
|
|
105
172
|
}
|
|
106
173
|
return ok(this.handle);
|
|
107
174
|
}
|
|
@@ -114,46 +181,72 @@ export class RemoteHostExecutionEnv {
|
|
|
114
181
|
async postResumeInit() {
|
|
115
182
|
return unsupported("postResumeInit");
|
|
116
183
|
}
|
|
184
|
+
/** The "connection" is the local process; there is nothing to re-establish. Idempotent success (unless destroyed). */
|
|
117
185
|
async reconnect(_sessionToken) {
|
|
118
186
|
if (this.destroyed)
|
|
119
187
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
120
188
|
return ok(this.handle);
|
|
121
189
|
}
|
|
190
|
+
/** `rm -rf` the per-task workspace (NOT the machine). Idempotent, best-effort, never throws (cleanup contract).
|
|
191
|
+
*
|
|
192
|
+
* core 1.258.2 destroy 契约注(remote-env.d.ts):非隔离 env(host lane,capabilities.isolation
|
|
193
|
+
* 缺席)的 destroy = 工作区/对象级清理,**不得收割仍在跑的后台进程**——收割全权归 `disposeBackgroundShells`
|
|
194
|
+
* (它带 timeout/session keep-alive except 名单)。此前这里的无-except 全灭把
|
|
195
|
+
* core 留活过的驻留 watcher 又杀了一遍(run 尾 dispose→destroy 顺序,destroy 是第二刀)。隔离 env(容器/VM)
|
|
196
|
+
* 整体拆机自然带走进程,与本契约不冲突(k8s/e2b 各自 destroy 不走这条路径)。 */
|
|
122
197
|
async destroy() {
|
|
123
198
|
if (this.destroyed)
|
|
124
|
-
return;
|
|
199
|
+
return; // idempotent — safe double-call (Runner end + reaper)
|
|
125
200
|
this.destroyed = true;
|
|
201
|
+
// Await any in-flight prepare so a destroy-vs-first-use race can't leak the workspace dir:
|
|
202
|
+
// an attached .then may still flip prepared=true after we null the memo. Then rm unconditionally
|
|
203
|
+
// (force:true no-ops a never-created path), eliminating the prepared-flag race entirely.
|
|
126
204
|
const p = this.preparing;
|
|
127
205
|
this.preparing = undefined;
|
|
128
206
|
try {
|
|
129
207
|
await p?.catch(() => { });
|
|
130
208
|
}
|
|
131
209
|
finally {
|
|
210
|
+
// spool 根(tmpdir 私有)与 workspace 归属无关——persistent 形也要清(五轮复审迁址后属本 env 自有)。
|
|
211
|
+
// 六轮复审:但驻留 shell(core 1.258.2「destroy 不得收割后台进程」契约,keep-alive except 名单)与
|
|
212
|
+
// terminal 残余(dispose 前仍可轮询)还在读写这些文件——有任何 shell 在册即跳过整树删,逐 shell 清理
|
|
213
|
+
// 归 dispose 面(state.dir 单删);空根目录留给 OS tmp 周期清,量级=一个空目录。
|
|
214
|
+
// 判据两条并列(见 liveFgSpools 旁注):后台 shell **或**在飞的前台 exec,任一还在就不删这个根 ——
|
|
215
|
+
// 删了等于把它们正在写的文件从脚下抽走,而写者拿的是 fd、察觉不到,只有读者看到"空输出"。
|
|
132
216
|
if (this.spoolRootDir && !this.bgManager.hasShells() && this.liveFgSpools.size === 0) {
|
|
133
217
|
try {
|
|
134
218
|
await fs.rm(this.spoolRootDir, { recursive: true, force: true });
|
|
135
219
|
}
|
|
136
|
-
catch { }
|
|
220
|
+
catch { /* best-effort */ }
|
|
137
221
|
this.spoolRootDir = undefined;
|
|
138
222
|
}
|
|
139
223
|
if (this.persistent)
|
|
140
|
-
return;
|
|
224
|
+
return; // user-owned persistent dir (TOC/CC) — NEVER rm -rf it
|
|
141
225
|
try {
|
|
142
226
|
await fs.rm(this.workspaceDir, { recursive: true, force: true });
|
|
143
227
|
}
|
|
144
228
|
catch {
|
|
229
|
+
/* best-effort: never throw on teardown */
|
|
145
230
|
}
|
|
146
231
|
}
|
|
147
232
|
}
|
|
148
233
|
async cleanup() {
|
|
149
234
|
return this.destroy();
|
|
150
235
|
}
|
|
236
|
+
// ─────────────────────────────── design/103 background shell (host lane) ───────────────────────────────
|
|
237
|
+
// Host lane = single-user TOC (the user's own box) → background shell is ON (run_in_background/BashOutput/KillShell
|
|
238
|
+
// auto-mount via core's `hasBackgroundShell`). `HOST_BG_ENABLED=false` turns it INERT (defence for a mis-configured
|
|
239
|
+
// multi-tenant host lane). caps tuned via HOST_BG_* env. The manager owns the red-lines; the driver below is local.
|
|
240
|
+
// A getter (NOT a field initializer) so it reads `this.cfg` which is set in the constructor — a field initializer
|
|
241
|
+
// runs before the constructor body and would see cfg undefined (the kata adapter hit the same ordering trap).
|
|
151
242
|
get backgroundCapabilities() {
|
|
152
243
|
return {
|
|
153
244
|
supported: (this.cfg.backgroundShell ?? true) && hostBackgroundShellEnabled(),
|
|
154
245
|
maxConcurrent: HOST_BG_MAX_CONCURRENT,
|
|
155
246
|
defaultBgTimeoutSec: HOST_BG_DEFAULT_TIMEOUT_SEC,
|
|
156
247
|
maxBgTimeoutSec: HOST_BG_MAX_TIMEOUT_SEC,
|
|
248
|
+
// core 1.207 design/116 detach (clay 排, 1.81): a running foreground exec can be ADOPTED as a background
|
|
249
|
+
// shell (mid-flight ctrl+b). Core's Bash tool gates the whole detach chain on this flag === true.
|
|
157
250
|
supportsDetach: (this.cfg.backgroundShell ?? true) && hostBackgroundShellEnabled(),
|
|
158
251
|
};
|
|
159
252
|
}
|
|
@@ -165,21 +258,25 @@ export class RemoteHostExecutionEnv {
|
|
|
165
258
|
const self = this;
|
|
166
259
|
return {
|
|
167
260
|
async launch(command, options, ctx) {
|
|
168
|
-
const ready = await self.shellReady();
|
|
261
|
+
const ready = await self.shellReady(); // lazily prepares the workspace dir
|
|
169
262
|
if (!ready.ok)
|
|
170
263
|
throw new BackgroundShellError("spawn_failed", ready.error.message);
|
|
171
264
|
const dir = path.join(self.ensureSpoolRoot(), "bg-" + randomBytes(8).toString("hex"));
|
|
172
265
|
await fs.mkdir(dir, { recursive: true });
|
|
173
266
|
const outPath = path.join(dir, "out");
|
|
174
267
|
const errPath = path.join(dir, "err");
|
|
268
|
+
// Open the child's stdout/stderr targets; the parent closes its OWN fd copies right after spawn so file
|
|
269
|
+
// size/EOF reflect only the child's writes (the child keeps its inherited fds).
|
|
175
270
|
const outFh = await fs.open(outPath, "w");
|
|
176
271
|
const errFh = await fs.open(errPath, "w");
|
|
177
272
|
let child;
|
|
178
273
|
try {
|
|
179
|
-
const sh = hostShell();
|
|
274
|
+
const sh = hostShell(); // Windows S1/S3: behind shellReady() above → resolved
|
|
180
275
|
child = spawn(sh.shell, [...sh.args, command], {
|
|
181
276
|
cwd: self.resolve(options?.cwd ?? self.cwd),
|
|
182
|
-
env: self.mergeEnv(options?.env),
|
|
277
|
+
env: self.mergeEnv(options?.env), // 🔴 SAME secret-scrub as exec (design/103 §3.1) — host process.env minus secret-pattern keys
|
|
278
|
+
// POSIX: detached:true → child = process-group leader (pgid === pid), kill(-pid) reaps the whole
|
|
279
|
+
// group. win32: no POSIX group either way — windowsHide, kill side = taskkill tree (design D3).
|
|
183
280
|
...spawnGroupOptions(),
|
|
184
281
|
stdio: ["ignore", outFh.fd, errFh.fd],
|
|
185
282
|
});
|
|
@@ -191,10 +288,18 @@ export class RemoteHostExecutionEnv {
|
|
|
191
288
|
throw new BackgroundShellError("spawn_failed", e instanceof Error ? e.message : String(e), e);
|
|
192
289
|
}
|
|
193
290
|
const pid = child.pid;
|
|
291
|
+
// 🔴 register close/error listeners SYNCHRONOUSLY (before any await) — a fast child must not exit before we
|
|
292
|
+
// listen, else onTerminal never fires and the shell would lie "running" until the timeout wall.
|
|
194
293
|
if (pid != null) {
|
|
294
|
+
// core-baseline terminal semantics (Fable-5 review #1): ANY self-ended process is "exited" with its exit
|
|
295
|
+
// code (signal death synthesizes the conventional 128+signo, like core's NodeExecutionEnv + the k8s/E2B
|
|
296
|
+
// lanes) — a non-zero exit is a NORMAL result, NOT "failed". "failed" is reserved for spawn-level errors.
|
|
297
|
+
// The old non-zero→failed classification hid the exit code from the model (BashOutput only prints
|
|
298
|
+
// `exited(code N)`), and split the three lanes' semantics. Classify on `close` (not `exit`), also per
|
|
299
|
+
// core baseline — stdio settled before the status flips (with fd-passed spool files close ≈ exit anyway).
|
|
195
300
|
child.on("close", (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0)));
|
|
196
|
-
child.on("error", () => ctx.onTerminal(true));
|
|
197
|
-
child.unref();
|
|
301
|
+
child.on("error", () => ctx.onTerminal(true)); // post-pid spawn error (rare)
|
|
302
|
+
child.unref(); // a detached background job must not keep the worker's event loop alive
|
|
198
303
|
}
|
|
199
304
|
await outFh.close().catch(() => { });
|
|
200
305
|
await errFh.close().catch(() => { });
|
|
@@ -205,6 +310,8 @@ export class RemoteHostExecutionEnv {
|
|
|
205
310
|
return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
|
|
206
311
|
},
|
|
207
312
|
async read(state) {
|
|
313
|
+
// ADOPT (memory) mode: drain the in-memory buffers (no files). Each read returns the queued chunks +
|
|
314
|
+
// resets the queue; totalBytes/droppedBytes are cursor metadata (design/103 §3.4).
|
|
208
315
|
if (state.mem) {
|
|
209
316
|
const o = drainMemStream(state.mem.out);
|
|
210
317
|
const e = drainMemStream(state.mem.err);
|
|
@@ -215,6 +322,9 @@ export class RemoteHostExecutionEnv {
|
|
|
215
322
|
bytesDroppedBeforeCursor: o.dropped + e.dropped,
|
|
216
323
|
};
|
|
217
324
|
}
|
|
325
|
+
// Returns the advanced cursor AND the TRUE on-disk size — the per-poll READ is capped at HOST_BG_READ_CAP,
|
|
326
|
+
// but the disk guard must see the real file size (a slow poller advances the cursor ≤1 MiB/poll while the
|
|
327
|
+
// file on the operator's real disk could already be GBs — review MED).
|
|
218
328
|
const readForward = async (p, cursor, dec) => {
|
|
219
329
|
let fh;
|
|
220
330
|
try {
|
|
@@ -222,93 +332,134 @@ export class RemoteHostExecutionEnv {
|
|
|
222
332
|
}
|
|
223
333
|
catch {
|
|
224
334
|
return { chunk: "", cursor, size: cursor };
|
|
225
|
-
}
|
|
335
|
+
} // file gone (disposed) → no new bytes
|
|
226
336
|
try {
|
|
227
337
|
const { size } = await fh.stat();
|
|
228
338
|
if (size <= cursor)
|
|
229
|
-
return { chunk: "", cursor: size, size };
|
|
339
|
+
return { chunk: "", cursor: size, size }; // no growth (or shrank) → cursor tracks size
|
|
230
340
|
const n = Math.min(size - cursor, HOST_BG_READ_CAP);
|
|
231
341
|
const buf = Buffer.alloc(n);
|
|
232
342
|
await fh.read(buf, 0, n, cursor);
|
|
343
|
+
// per-shell StringDecoder([1116] 批):跨段码点保状态——逐段 toString 会在段界劈出 U+FFFD。
|
|
233
344
|
return { chunk: dec.write(buf), cursor: cursor + n, size };
|
|
234
345
|
}
|
|
235
346
|
finally {
|
|
236
347
|
await fh.close().catch(() => { });
|
|
237
348
|
}
|
|
238
349
|
};
|
|
350
|
+
// File mode (launch + [1116] spool adoption): the memory-mode early-return above handled `state.mem`.
|
|
239
351
|
state.decOut ??= new StringDecoder("utf8");
|
|
240
352
|
state.decErr ??= new StringDecoder("utf8");
|
|
241
353
|
const o = await readForward(state.outPath, state.stdoutCursor, state.decOut);
|
|
242
354
|
const e = await readForward(state.errPath, state.stderrCursor, state.decErr);
|
|
243
355
|
state.stdoutCursor = o.cursor;
|
|
244
356
|
state.stderrCursor = e.cursor;
|
|
357
|
+
// Disk guard: reap a runaway by its TRUE on-disk size (the host lane has no VM isolation to contain a
|
|
358
|
+
// runaway output file; the BG-timeout also bounds wall-clock).
|
|
245
359
|
let terminal;
|
|
246
360
|
if (o.size > HOST_BG_FILE_CAP || e.size > HOST_BG_FILE_CAP) {
|
|
247
361
|
try {
|
|
248
362
|
killTreeHard(state.pid);
|
|
249
363
|
}
|
|
250
|
-
catch { }
|
|
364
|
+
catch { /* already dead */ } // disk-guard熔断 (the 5th kill site) — POSIX kill(-pid) byte-identical; win32 taskkill /F /T
|
|
251
365
|
terminal = { failed: true };
|
|
252
366
|
}
|
|
367
|
+
// F2/F3([1116] 批,二轮复审):spool adoption 携带的 fg 期文件移除字节。preSpoolBase 每拍并入
|
|
368
|
+
// bytesFromStart(单调不倒退);pendingDropped 只首拍并入 bytesDroppedBeforeCursor 后清零(launch
|
|
369
|
+
// 形两者恒 undefined→0:本地文件不 head-evict,全量保留到 dispose)。
|
|
253
370
|
const preBase = state.preSpoolBase ?? 0;
|
|
254
371
|
const pending = state.pendingDropped ?? 0;
|
|
255
372
|
state.pendingDropped = 0;
|
|
256
373
|
return {
|
|
257
374
|
stdout: o.chunk,
|
|
258
375
|
stderr: e.chunk,
|
|
259
|
-
bytesFromStart: o.size + e.size + preBase,
|
|
376
|
+
bytesFromStart: o.size + e.size + preBase, // TRUE total from start(含 fg 已移除段;preBase 每拍恒在)
|
|
260
377
|
bytesDroppedBeforeCursor: pending,
|
|
261
378
|
...(terminal ? { terminal } : {}),
|
|
262
379
|
};
|
|
263
380
|
},
|
|
264
381
|
async kill(state) {
|
|
382
|
+
// host lane has NO container isolation — a SIGTERM-trapping group must still die at the hard wall. Send
|
|
383
|
+
// SIGTERM, then escalate to SIGKILL after a short grace (mirrors kata's `kill -TERM; sleep 1; kill -KILL`).
|
|
384
|
+
// This is the path for BOTH KillShell and the manager's BG-timeout wall, so the §3.6 "到点自杀" really kills.
|
|
385
|
+
// POSIX byte-identical: killTreeSoft = kill(-pid, SIGTERM) incl. the throw-on-dead-group → return (no
|
|
386
|
+
// escalation needed). win32 (design D2): soft = taskkill /T — a no-op for console trees ("can only be
|
|
387
|
+
// terminated forcefully"), and it never throws, so the escalation timer ALWAYS arms there: the ladder
|
|
388
|
+
// degrades to delay→hard-kill, which is exactly the accepted win32 semantics.
|
|
265
389
|
try {
|
|
266
390
|
killTreeSoft(state.pid);
|
|
267
391
|
}
|
|
268
392
|
catch {
|
|
269
393
|
return;
|
|
270
|
-
}
|
|
394
|
+
} // idempotent: already-dead group → no escalation needed
|
|
271
395
|
setTimeout(() => { try {
|
|
272
396
|
killTreeHard(state.pid);
|
|
273
397
|
}
|
|
274
|
-
catch { } }, HOST_BG_KILL_GRACE_MS).unref?.();
|
|
398
|
+
catch { /* exited within grace */ } }, HOST_BG_KILL_GRACE_MS).unref?.();
|
|
275
399
|
},
|
|
276
400
|
async disposeOne(state) {
|
|
277
|
-
state.detachCleanup?.();
|
|
401
|
+
state.detachCleanup?.(); // adopt path: detach the extra pipe listeners (memory mode → no fds/dir)
|
|
278
402
|
try {
|
|
279
403
|
killTreeHard(state.pid);
|
|
280
404
|
}
|
|
281
|
-
catch { }
|
|
405
|
+
catch { /* already dead */ }
|
|
282
406
|
if (state.dir)
|
|
283
|
-
await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { });
|
|
407
|
+
await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { }); // file (launch) mode only
|
|
284
408
|
},
|
|
285
409
|
};
|
|
286
410
|
}
|
|
287
411
|
spawnBackground(command, options) {
|
|
288
412
|
return this.bgManager.spawn(command, options);
|
|
289
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* design/116 detach (core 1.207, clay 排 1.81): ADOPT a still-running FOREGROUND exec child as a background
|
|
416
|
+
* shell — the mid-flight ctrl+b path. The exec child writes to pipes (not files, unlike a `launch`-spawned
|
|
417
|
+
* bg shell), so adoption splices those live pipes into fresh bg spool files: pre-fill them with the output
|
|
418
|
+
* captured so far (the detach TEE, fed from the exec's FIRST chunk → TaskOutput's first read has the full
|
|
419
|
+
* history), then append every subsequent chunk. From then on the shell is indistinguishable from a spawned one
|
|
420
|
+
* (poll by cursor, kill by pgid, the BG-timeout wall applies). Returns the opaque shellId, or `undefined` when
|
|
421
|
+
* the live-shell limit is hit (detach refused → the exec keeps running in the foreground; NEVER a dropped process).
|
|
422
|
+
*
|
|
423
|
+
* `memOut`/`memErr` are the exec's tee-from-start buffers (Fable-5 review #3, mirrors the E2B tee): they were fed
|
|
424
|
+
* every chunk since process start THROUGH THE SAME per-stream StringDecoder that keeps feeding post-detach, so a
|
|
425
|
+
* multibyte codepoint straddling the detach instant decodes correctly (the old seed-from-fg-tail handoff decoded
|
|
426
|
+
* the two halves in separate passes → U+FFFD at the seam).
|
|
427
|
+
*/
|
|
290
428
|
adoptRunningExec(child, memOut, memErr) {
|
|
291
429
|
const pid = child.pid;
|
|
292
430
|
if (pid == null)
|
|
293
|
-
return undefined;
|
|
431
|
+
return undefined; // no pid → nothing to adopt (exec keeps running / will settle normally)
|
|
432
|
+
// 🔴 FULLY SYNCHRONOUS (double-review, 3 findings): memory buffers, no files/fds. The tee handover + the
|
|
433
|
+
// live-pipe listener attach happen in ONE tick so no output is lost to an async window, and the exec-settle
|
|
434
|
+
// can't race the adoption (mirrors core's `adoptRunningChild`).
|
|
435
|
+
// 🔴 builder contract (Fable-5 review #5): everything below is NON-THROWING (listener attach + plain object
|
|
436
|
+
// construction). adoptSync's catch returns undefined WITHOUT undoing side effects — any future logic that can
|
|
437
|
+
// throw must go BEFORE the first side effect (listener/on) or clean up after itself.
|
|
294
438
|
const adopted = this.bgManager.adoptSync((ctx) => {
|
|
295
439
|
const mem = { out: memOut, err: memErr };
|
|
440
|
+
// Bound un-drained memory (no VM on the host lane to contain a runaway): drop OLDEST, count as truncated.
|
|
296
441
|
const onOut = (d) => feedMemStream(mem.out, d, HOST_BG_MEM_CAP);
|
|
297
442
|
const onErr = (d) => feedMemStream(mem.err, d, HOST_BG_MEM_CAP);
|
|
298
443
|
child.stdout?.on("data", onOut);
|
|
299
444
|
child.stderr?.on("data", onErr);
|
|
445
|
+
// core-baseline terminal semantics (Fable-5 review #1, same as the launch path): self-ended → "exited" with
|
|
446
|
+
// the exit code (signal death = 128+signo), NEVER "failed" — matches core's adoptRunningChild verbatim.
|
|
447
|
+
// Classify on `close` (not `exit`), also per core baseline: in pipe (adopt) mode the final data events can
|
|
448
|
+
// trail `exit` — flipping the status early lets a poll in that window read "exited" with the tail undrained.
|
|
300
449
|
const onClose = (code, signal) => ctx.onTerminal(false, code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0));
|
|
301
450
|
const onErrEvt = () => ctx.onTerminal(true);
|
|
302
451
|
child.on("close", onClose);
|
|
303
452
|
child.on("error", onErrEvt);
|
|
453
|
+
// Race guard: the child may have exited between the detach request and adoption — a close that fired before
|
|
454
|
+
// the listener attached won't re-fire, so classify terminal now (mirrors core's adoptRunningChild).
|
|
304
455
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
305
456
|
ctx.onTerminal(false, child.exitCode ?? (child.signalCode ? 128 + (signalNumber(child.signalCode) ?? 9) : 0));
|
|
306
|
-
child.unref?.();
|
|
457
|
+
child.unref?.(); // a detached background job must not keep the worker's event loop alive
|
|
307
458
|
const detachCleanup = () => {
|
|
308
459
|
child.stdout?.off("data", onOut);
|
|
309
460
|
child.stderr?.off("data", onErr);
|
|
310
461
|
child.off("close", onClose);
|
|
311
|
-
child.off("error", onErrEvt);
|
|
462
|
+
child.off("error", onErrEvt); // no fds to close — memory mode
|
|
312
463
|
};
|
|
313
464
|
return { pid, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
|
|
314
465
|
});
|
|
@@ -321,8 +472,18 @@ export class RemoteHostExecutionEnv {
|
|
|
321
472
|
return this.bgManager.kill(shellId);
|
|
322
473
|
}
|
|
323
474
|
disposeBackgroundShells(opts) {
|
|
475
|
+
// 签名带 opts——core runtask 尾调 `{except: keepAlive}`(timeout/session 驻留 Monitor
|
|
476
|
+
// 留活名单),此前无参签名把它静默丢弃=kill-all。透传给 manager(无参=既有全灭)。
|
|
324
477
|
return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
|
|
325
478
|
}
|
|
479
|
+
// ─────────────────────────────── Shell: buffered exec ───────────────────────────────
|
|
480
|
+
//
|
|
481
|
+
// [1116] fg spool 化(core 1.318 修的平行盲区,当班移植):管道 stdio + 等 `close` settle 的旧形,
|
|
482
|
+
// 在孙进程(`nohup server &`)持有管道写端时 close 恒不发——exec 挂满 timeout → 组杀连坐孙进程。
|
|
483
|
+
// spool 形:子进程 stdout/stderr 直写文件 fd(进程间零管道),`close` 仍是主 settle 路,但 `exit`
|
|
484
|
+
// +500ms drain 兜底(close 不来也 settle);正常终态不进任何杀伤集(timeout/abort 才杀树)。
|
|
485
|
+
// detach adoption 走 FILE 形 handover(spool 路径+游标直接进 HostBgState——launch 同形,poll 共码,
|
|
486
|
+
// 单一字节流天然无 decoder 接缝)。win32 / spool 打开失败 → 回退管道旧形(core 同姿势)。
|
|
326
487
|
async exec(command, options) {
|
|
327
488
|
if (options?.abortSignal?.aborted)
|
|
328
489
|
return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
|
|
@@ -331,7 +492,11 @@ export class RemoteHostExecutionEnv {
|
|
|
331
492
|
return ready;
|
|
332
493
|
const cwd = this.resolve(options?.cwd ?? this.cwd);
|
|
333
494
|
const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
|
|
495
|
+
// core 1.207 design/116 detach: the Bash tool threads a `detachSignal` when the model/operator mid-flight
|
|
496
|
+
// ctrl+b's a running command. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally.
|
|
334
497
|
const detachSignal = options?.detachSignal;
|
|
498
|
+
// [1116] spool 形优先(POSIX;HOST_EXEC_SPOOL_ENABLED=false 逃生舱)。打开失败=回退管道旧形,永不因
|
|
499
|
+
// spool 基建挡 exec。
|
|
335
500
|
if (process.platform !== "win32" && hostExecSpoolEnabled()) {
|
|
336
501
|
const spool = this.openFgSpool();
|
|
337
502
|
if (spool)
|
|
@@ -339,19 +504,37 @@ export class RemoteHostExecutionEnv {
|
|
|
339
504
|
}
|
|
340
505
|
return this.execViaPipes(command, options, cwd, timeoutMs, detachSignal);
|
|
341
506
|
}
|
|
507
|
+
/** 管道旧形(win32 / spool 打开失败的回退;行为=1.225 及以前的 exec 原样)。 */
|
|
342
508
|
execViaPipes(command, options, cwd, timeoutMs, detachSignal) {
|
|
343
509
|
return new Promise((resolve) => {
|
|
510
|
+
// D2: bounded rolling-TAIL (core's RollingTailBuffer) — a model-driven `yes`/`cat big.bin` can't OOM the
|
|
511
|
+
// durable worker; keep only the last 8MB (tail = errors+exit, what the model needs) + a dropped-byte marker. O(maxBytes).
|
|
344
512
|
const outBuf = new RollingTailBuffer();
|
|
345
513
|
const errBuf = new RollingTailBuffer();
|
|
514
|
+
// design/116 detach TEE (mirrors the E2B tee-from-start): when the exec is detach-capable,
|
|
515
|
+
// tee every chunk from process START into the pair of drain buffers a later adoption hands over as-is. The
|
|
516
|
+
// per-stream StringDecoder inside BgMemStream then spans the whole stream — fg segment AND post-detach segment
|
|
517
|
+
// decode through ONE stateful decoder, so a multibyte codepoint straddling the detach instant survives (the
|
|
518
|
+
// old fg-tail→seed handoff decoded the halves separately → U+FFFD at the seam). Cost: one extra bounded
|
|
519
|
+
// (drop-oldest, HOST_BG_MEM_CAP) buffer pair for the exec's lifetime, only when a detachSignal is present.
|
|
346
520
|
const tee = detachSignal ? { out: seedMemStream(""), err: seedMemStream("") } : undefined;
|
|
347
521
|
let settled = false;
|
|
522
|
+
// D5 pipe-destroy grace: armed inside kill() (abort/timeout); destroys the stdout/stderr pipes if `close`
|
|
523
|
+
// never fires (a re-parented grandchild holding the FD). Cleared in finish() — including the real `close`
|
|
524
|
+
// path, which routes through finish() — so the timer never leaks. Does NOT change promise settle (the
|
|
525
|
+
// abort/timeout handlers already finish() synchronously below).
|
|
348
526
|
let forceSettleTimer;
|
|
527
|
+
// Windows S1/S3 (design D1/D3): shell + group options are platform-resolved (POSIX byte-identical:
|
|
528
|
+
// /bin/sh + detached:true so kill(-pid) reaps the whole group; win32: Git Bash + windowsHide, kill
|
|
529
|
+
// side uses the taskkill tree instead of a group).
|
|
349
530
|
const sh = hostShell();
|
|
350
531
|
const child = spawn(sh.shell, [...sh.args, command], {
|
|
351
532
|
cwd,
|
|
352
533
|
env: this.mergeEnv(options?.env),
|
|
353
534
|
...spawnGroupOptions(),
|
|
354
535
|
});
|
|
536
|
+
// core 1.321 cut-kill registry(三轮复审存量缺口):把杀柄交给上层(Bash 工具 cut 时强制终杀);
|
|
537
|
+
// 柄内 still-running 守卫(exitCode/signalCode)防对已亡组的 pgid 复用误杀(core 参考实现同语义)。
|
|
355
538
|
if (options?.onSpawn && child.pid != null) {
|
|
356
539
|
const spawnedPid = child.pid;
|
|
357
540
|
try {
|
|
@@ -365,18 +548,23 @@ export class RemoteHostExecutionEnv {
|
|
|
365
548
|
try {
|
|
366
549
|
child.kill("SIGKILL");
|
|
367
550
|
}
|
|
368
|
-
catch { }
|
|
551
|
+
catch { /* noop */ }
|
|
369
552
|
}
|
|
370
553
|
} });
|
|
371
554
|
}
|
|
372
|
-
catch { }
|
|
555
|
+
catch { /* registry 回调失败不挡 exec(core 同姿势吞) */ }
|
|
373
556
|
}
|
|
374
557
|
const finish = (r) => {
|
|
375
558
|
if (settled)
|
|
376
559
|
return;
|
|
377
560
|
settled = true;
|
|
378
561
|
clearTimeout(timer);
|
|
562
|
+
// NOTE: the D5 grace timer is cleared in the `close` handler (pipes closed naturally), NOT here — an
|
|
563
|
+
// abort/timeout settle reaches finish() BEFORE `close`, so clearing here would cancel the grace before
|
|
564
|
+
// it can free a leaked FD (the exact case it exists for). On abort/timeout it fires after the grace.
|
|
379
565
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
566
|
+
// A foreground settle must also release the never-fired detach listener — the hub's signal
|
|
567
|
+
// can outlive this exec (bounded 256-entry hub), and the listener closes over the child + tee buffers.
|
|
380
568
|
removeDetachListener?.();
|
|
381
569
|
resolve(r);
|
|
382
570
|
};
|
|
@@ -384,14 +572,17 @@ export class RemoteHostExecutionEnv {
|
|
|
384
572
|
const kill = () => {
|
|
385
573
|
try {
|
|
386
574
|
if (child.pid != null)
|
|
387
|
-
killTreeHard(child.pid);
|
|
575
|
+
killTreeHard(child.pid); // POSIX: kill(-pid) whole group (byte-identical); win32: taskkill /F /T
|
|
388
576
|
}
|
|
389
577
|
catch {
|
|
390
578
|
try {
|
|
391
579
|
child.kill("SIGKILL");
|
|
392
580
|
}
|
|
393
|
-
catch { }
|
|
581
|
+
catch { /* noop */ }
|
|
394
582
|
}
|
|
583
|
+
// D5: if the kill leaves a re-parented grandchild holding the pipes, `close` may never come — free the
|
|
584
|
+
// stdout/stderr FDs after a short grace. Idempotent (armPipeDestroyGrace is a no-op if already settled →
|
|
585
|
+
// we only ever arm one; reassigning is harmless since kill is called once per abort/timeout).
|
|
395
586
|
forceSettleTimer ??= armPipeDestroyGrace(child);
|
|
396
587
|
};
|
|
397
588
|
const timer = setTimeout(() => {
|
|
@@ -404,38 +595,56 @@ export class RemoteHostExecutionEnv {
|
|
|
404
595
|
};
|
|
405
596
|
if (options?.abortSignal)
|
|
406
597
|
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
598
|
+
// core 1.207 design/116 detach (clay 排, 1.81): mid-flight ctrl+b — ADOPT the running child as a background
|
|
599
|
+
// shell instead of killing it. On adoption the exec settles EARLY with `detached:{shellId}` (+ the output
|
|
600
|
+
// captured so far); ownership (output capture → the bg files, close handling, the BG-timeout wall, kill)
|
|
601
|
+
// moves to the bgManager entry. Fired after `settled` = too late (no-op). Refused over the live-shell limit
|
|
602
|
+
// (or on a pid-less/failed adopt) → the exec keeps running in the foreground, settling normally on close.
|
|
407
603
|
const onDetach = () => {
|
|
408
604
|
if (settled)
|
|
409
605
|
return;
|
|
410
606
|
const out = outBuf.result();
|
|
411
607
|
const err = errBuf.result();
|
|
608
|
+
// 🔴 SYNCHRONOUS adopt (double-review): no `await` between the tee handover and the pipe-listener attach,
|
|
609
|
+
// so no data chunk is lost and no exec close/timeout can settle mid-adoption. The exec's ORIGINAL stdout/
|
|
610
|
+
// stderr listeners are removed FIRST so post-detach output flows only to the adopted memory buffers (no
|
|
611
|
+
// double-count into the now-orphaned outBuf) and `close` won't re-drive them. The adopted spool is the
|
|
612
|
+
// tee-from-start pair (already holds every chunk since process start through ONE stateful decoder — review
|
|
613
|
+
// #3); the truncation marker rides on the exec's OWN result string only (review LOW).
|
|
412
614
|
child.stdout?.off("data", onExecOut);
|
|
413
615
|
child.stderr?.off("data", onExecErr);
|
|
414
616
|
const shellId = this.adoptRunningExec(child, tee.out, tee.err);
|
|
415
617
|
const seedOut = markTruncated(out.text, out.droppedBytes);
|
|
416
618
|
const seedErr = markTruncated(err.text, err.droppedBytes);
|
|
417
619
|
if (shellId === undefined) {
|
|
620
|
+
// Refused (limit) or no pid → re-attach the exec listeners; the command keeps running in the foreground
|
|
621
|
+
// and settles normally on close (NEVER a dropped process).
|
|
418
622
|
child.stdout?.on("data", onExecOut);
|
|
419
623
|
child.stderr?.on("data", onExecErr);
|
|
420
624
|
return;
|
|
421
625
|
}
|
|
422
|
-
clearTimeout(timer);
|
|
626
|
+
clearTimeout(timer); // the adopted process is the bgManager's now (its own BG-timeout wall applies)
|
|
423
627
|
if (forceSettleTimer)
|
|
424
628
|
clearTimeout(forceSettleTimer);
|
|
425
629
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
426
630
|
settled = true;
|
|
631
|
+
// cut-kill registry 注销(所有权已移交 bgManager——cut 不该再杀后台 shell;core 参考实现同位)。
|
|
427
632
|
try {
|
|
428
633
|
options?.onDetachAdopted?.();
|
|
429
634
|
}
|
|
430
|
-
catch { }
|
|
635
|
+
catch { /* noop */ }
|
|
431
636
|
resolve(ok({ stdout: seedOut, stderr: seedErr, exitCode: 0, detached: { shellId } }));
|
|
432
637
|
};
|
|
638
|
+
// Named (not inline) so onDetach can `off` them when it hands the pipes to the adopted background shell.
|
|
433
639
|
const onExecOut = (d) => { outBuf.push(d); if (tee)
|
|
434
640
|
feedMemStream(tee.out, d, HOST_BG_MEM_CAP); options?.onStdout?.(d.toString()); };
|
|
435
641
|
const onExecErr = (d) => { errBuf.push(d); if (tee)
|
|
436
642
|
feedMemStream(tee.err, d, HOST_BG_MEM_CAP); options?.onStderr?.(d.toString()); };
|
|
437
643
|
child.stdout?.on("data", onExecOut);
|
|
438
644
|
child.stderr?.on("data", onExecErr);
|
|
645
|
+
// 🔴 AFTER the exec listeners exist (TDZ): core's ToolDetachHub uses create-then-abort, so a detach that
|
|
646
|
+
// landed BEFORE this exec started arrives as an ALREADY-aborted signal → onDetach runs synchronously right
|
|
647
|
+
// here and must be able to `off`/re-`on` onExecOut/onExecErr. Same tick either way — no event can interleave.
|
|
439
648
|
if (detachSignal) {
|
|
440
649
|
if (detachSignal.aborted)
|
|
441
650
|
onDetach();
|
|
@@ -445,11 +654,16 @@ export class RemoteHostExecutionEnv {
|
|
|
445
654
|
}
|
|
446
655
|
}
|
|
447
656
|
child.on("error", (e) => {
|
|
657
|
+
// Failure to even spawn the shell (ENOENT etc.) — never the command's own non-zero exit.
|
|
448
658
|
finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`, e) });
|
|
449
659
|
});
|
|
450
660
|
child.on("close", (code, signal) => {
|
|
661
|
+
// `close` fired ⇒ the pipes closed naturally → clear the D5 grace (it only needs to force-destroy FDs when
|
|
662
|
+
// `close` NEVER comes after a kill — the re-parented-grandchild leak). Clearing here, not in finish().
|
|
451
663
|
if (forceSettleTimer)
|
|
452
664
|
clearTimeout(forceSettleTimer);
|
|
665
|
+
// A non-zero exit is a NORMAL value, not an error (ExecutionEnv contract). A kill-by-signal that
|
|
666
|
+
// wasn't a timeout/abort still resolves with the conventional 128+signo exit code.
|
|
453
667
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
|
|
454
668
|
const out = outBuf.result();
|
|
455
669
|
const err = errBuf.result();
|
|
@@ -457,6 +671,8 @@ export class RemoteHostExecutionEnv {
|
|
|
457
671
|
});
|
|
458
672
|
});
|
|
459
673
|
}
|
|
674
|
+
/** [1116] fg spool pair:tmpdir 私有根下专属子目录(五轮复审迁址,workspace=命令可写区不可放)+ "ax"
|
|
675
|
+
* 独占建(core openSpoolPair 同形)。失败=undefined(调用方回退管道形)。 */
|
|
460
676
|
openFgSpool() {
|
|
461
677
|
try {
|
|
462
678
|
const dir = path.join(this.ensureSpoolRoot(), "fg-" + randomBytes(8).toString("hex"));
|
|
@@ -472,16 +688,17 @@ export class RemoteHostExecutionEnv {
|
|
|
472
688
|
try {
|
|
473
689
|
closeSync(fdOut);
|
|
474
690
|
}
|
|
475
|
-
catch { }
|
|
691
|
+
catch { /* noop */ }
|
|
476
692
|
throw e;
|
|
477
693
|
}
|
|
478
|
-
this.liveFgSpools.add(dir);
|
|
694
|
+
this.liveFgSpools.add(dir); // 见 liveFgSpools 旁注:登记后 destroy() 才不会把它脚下的根删掉
|
|
479
695
|
return { dir, outPath, errPath, fds: [fdOut, fdErr] };
|
|
480
696
|
}
|
|
481
697
|
catch {
|
|
482
698
|
return undefined;
|
|
483
699
|
}
|
|
484
700
|
}
|
|
701
|
+
/** [1116] spool 形 fg exec(core node-execution-env 1.318-1.321 机制移植;坑图=core CHANGELOG 同段)。 */
|
|
485
702
|
execViaSpool(command, options, cwd, timeoutMs, detachSignal, spool) {
|
|
486
703
|
const EXIT_SETTLE_DRAIN_MS = 500;
|
|
487
704
|
const PUMP_MS = 200;
|
|
@@ -492,10 +709,14 @@ export class RemoteHostExecutionEnv {
|
|
|
492
709
|
const outBuf = new RollingTailBuffer();
|
|
493
710
|
const errBuf = new RollingTailBuffer();
|
|
494
711
|
let settled = false;
|
|
495
|
-
let handedOver = false;
|
|
712
|
+
let handedOver = false; // detach adoption 拿走了 spool 文件(bg poll 续读)——settle 清理不删文件
|
|
496
713
|
let pump;
|
|
497
714
|
let drainTimer;
|
|
498
715
|
const cursor = { out: 0, err: 0 };
|
|
716
|
+
// F2(二轮复审):从命名 spool **文件**移除的累计字节(仅轮转/硬帽 truncate;skipAhead 不算,
|
|
717
|
+
// 字节还在文件里)——与
|
|
718
|
+
// RollingTailBuffer.droppedBytes 独立(后者只反映内存尾窗的 head-evict,轮转 truncate 后尾窗仍满
|
|
719
|
+
// =0 dropped,整代对文件消失却不计)。handover 从 0 读现存文件,这个数就是 bg 看不到的历史。
|
|
499
720
|
const spoolRemoved = { out: 0, err: 0 };
|
|
500
721
|
const lanes = [
|
|
501
722
|
{ path: spool.outPath, key: "out", buf: outBuf, cb: options?.onStdout },
|
|
@@ -531,25 +752,33 @@ export class RemoteHostExecutionEnv {
|
|
|
531
752
|
return;
|
|
532
753
|
const skipped = behind - keep;
|
|
533
754
|
lane.buf.recordSkippedBytes(skipped);
|
|
755
|
+
// 注意:skip 只推游标,字节仍留在命名文件里(handover 从 0 读**看得到**)——不计 spoolRemoved;
|
|
756
|
+
// 它们随后被 truncate 抹掉时按 cursor 整段计入(三轮复审:此处预计会双计)。
|
|
534
757
|
cursor[lane.key] += skipped;
|
|
535
758
|
}
|
|
536
|
-
catch { }
|
|
759
|
+
catch { /* noop */ }
|
|
537
760
|
};
|
|
538
761
|
const reclaim = (lane) => {
|
|
762
|
+
// 轮转回收(core 坑图「配额绕过」形):游标推进到 ROTATE 以上且文件读平 → truncate 归零;文件冲破
|
|
763
|
+
// 硬帽(泵落后)→ 记 skipped 后强制归零(磁盘上限=诚实丢最旧)。
|
|
539
764
|
if (cursor[lane.key] < ROTATE_BYTES)
|
|
540
765
|
return;
|
|
541
766
|
try {
|
|
542
767
|
const size = statSync(lane.path).size;
|
|
543
768
|
if (size === cursor[lane.key] || size >= HARD_CAP_BYTES) {
|
|
769
|
+
// F2 竞窗补账(三轮复审收紧):truncate 前复采 size2(stat→truncate 间 "ax" append 形可能又写)
|
|
770
|
+
// → 记账全部挪到 truncateSync **成功之后**(throw=文件一字未动=不记 spoolRemoved,下拍重试);
|
|
771
|
+
// 实际截除=size2 整段(cursor 已 pump 部分 + 未 pump 部分)。size2 采样与 truncate 之间的极窄
|
|
772
|
+
// append 窗仍在(活跃写者下 truncate 语义固有;量级=两条相邻 syscall 间),接受为残余误差。
|
|
544
773
|
const size2 = statSync(lane.path).size;
|
|
545
774
|
truncateSync(lane.path, 0);
|
|
546
775
|
if (size2 > cursor[lane.key])
|
|
547
|
-
lane.buf.recordSkippedBytes(size2 - cursor[lane.key]);
|
|
548
|
-
spoolRemoved[lane.key] += size2;
|
|
776
|
+
lane.buf.recordSkippedBytes(size2 - cursor[lane.key]); // 未 pump 段对内存尾窗=跳过
|
|
777
|
+
spoolRemoved[lane.key] += size2; // 硬帽/轮转同式:文件真正归零的字节数
|
|
549
778
|
cursor[lane.key] = 0;
|
|
550
779
|
}
|
|
551
780
|
}
|
|
552
|
-
catch { }
|
|
781
|
+
catch { /* noop */ }
|
|
553
782
|
};
|
|
554
783
|
const feed = (lane, inc) => {
|
|
555
784
|
lane.buf.push(inc);
|
|
@@ -557,7 +786,7 @@ export class RemoteHostExecutionEnv {
|
|
|
557
786
|
try {
|
|
558
787
|
lane.cb?.(inc.toString("utf8"));
|
|
559
788
|
}
|
|
560
|
-
catch { }
|
|
789
|
+
catch { /* callback 失败不挡 settle(host 旧形同容忍) */ }
|
|
561
790
|
}
|
|
562
791
|
};
|
|
563
792
|
const pumpOnce = () => {
|
|
@@ -570,6 +799,7 @@ export class RemoteHostExecutionEnv {
|
|
|
570
799
|
}
|
|
571
800
|
};
|
|
572
801
|
const drainFinal = () => {
|
|
802
|
+
// finalization 真尾部(core 坑图:快突发不再拿头丢尾)——先跳到只剩 FINAL_TAIL 窗,再最多 4 轮读平。
|
|
573
803
|
for (const lane of lanes) {
|
|
574
804
|
skipAhead(lane, FINAL_TAIL_BYTES);
|
|
575
805
|
for (let i = 0; i < 4; i++) {
|
|
@@ -589,21 +819,23 @@ export class RemoteHostExecutionEnv {
|
|
|
589
819
|
clearTimeout(drainTimer);
|
|
590
820
|
drainTimer = undefined;
|
|
591
821
|
}
|
|
592
|
-
this.liveFgSpools.delete(spool.dir);
|
|
822
|
+
this.liveFgSpools.delete(spool.dir); // 注销:本 exec 已 settle,它不再需要这个根活着
|
|
593
823
|
if (!handedOver) {
|
|
824
|
+
// RB-26 同型记档(core 官方裁决,发版复审 F3 对拆):幸存孙进程持继承 fd 对 unlinked inode 的
|
|
825
|
+
// 后续写=脱离进程自身的增长,接受不管(命名文件形=CC 同暴露且路径可见更差;进程退出即回收)。
|
|
594
826
|
for (const lane of lanes) {
|
|
595
827
|
try {
|
|
596
828
|
unlinkSync(lane.path);
|
|
597
829
|
}
|
|
598
|
-
catch { }
|
|
830
|
+
catch { /* noop */ }
|
|
599
831
|
}
|
|
600
832
|
try {
|
|
601
833
|
rmdirSync(spool.dir);
|
|
602
834
|
}
|
|
603
|
-
catch { }
|
|
835
|
+
catch { /* noop */ }
|
|
604
836
|
}
|
|
605
837
|
};
|
|
606
|
-
let removeDetachListener;
|
|
838
|
+
let removeDetachListener; // F5(二轮复审):先声明——onAbort→finish 会读它,TDZ 会抛 ReferenceError
|
|
607
839
|
const finish = (r) => {
|
|
608
840
|
if (settled)
|
|
609
841
|
return;
|
|
@@ -619,7 +851,7 @@ export class RemoteHostExecutionEnv {
|
|
|
619
851
|
try {
|
|
620
852
|
closeSync(fd);
|
|
621
853
|
}
|
|
622
|
-
catch { }
|
|
854
|
+
catch { /* noop */ }
|
|
623
855
|
}
|
|
624
856
|
};
|
|
625
857
|
const sh = hostShell();
|
|
@@ -629,15 +861,18 @@ export class RemoteHostExecutionEnv {
|
|
|
629
861
|
cwd,
|
|
630
862
|
env: this.mergeEnv(options?.env),
|
|
631
863
|
...spawnGroupOptions(),
|
|
632
|
-
stdio: ["ignore", spool.fds[0], spool.fds[1]],
|
|
864
|
+
stdio: ["ignore", spool.fds[0], spool.fds[1]], // 子进程直写文件——进程间零管道([1116] 病理根除点)
|
|
633
865
|
});
|
|
634
866
|
}
|
|
635
867
|
catch (e) {
|
|
868
|
+
// 同步 spawn throw(如 cwd=文件的 ENOTDIR)——与管道旧形字节级对齐:executor 内 re-throw=Promise
|
|
869
|
+
// reject(消费方契约:core pruneWorktrees 等 await 抛才算失败;吞成 Result err 会让 reap 静默)。
|
|
636
870
|
closeSpoolFds();
|
|
637
871
|
cleanupSpool();
|
|
638
872
|
throw e;
|
|
639
873
|
}
|
|
640
|
-
closeSpoolFds();
|
|
874
|
+
closeSpoolFds(); // 父进程的 fd 副本立即关(文件 size/EOF 只反映子进程写;launch FILE 形同姿势)
|
|
875
|
+
// core 1.321 cut-kill registry(三轮复审存量缺口):同 pipe 径——杀柄带 still-running 守卫。
|
|
641
876
|
if (options?.onSpawn && child.pid != null) {
|
|
642
877
|
const spawnedPid = child.pid;
|
|
643
878
|
try {
|
|
@@ -651,12 +886,16 @@ export class RemoteHostExecutionEnv {
|
|
|
651
886
|
try {
|
|
652
887
|
child.kill("SIGKILL");
|
|
653
888
|
}
|
|
654
|
-
catch { }
|
|
889
|
+
catch { /* noop */ }
|
|
655
890
|
}
|
|
656
891
|
} });
|
|
657
892
|
}
|
|
658
|
-
catch { }
|
|
893
|
+
catch { /* registry 回调失败不挡 exec */ }
|
|
659
894
|
}
|
|
895
|
+
// 七轮复审:'error' 监听必须在任何可提前 return 的路径(下方 F5 already-aborted 查)之前装——异步
|
|
896
|
+
// spawn 失败(如 cwd 不存在)以 ChildProcess 'error' 事件发出,无监听=uncaught 直接打死 worker 进程
|
|
897
|
+
// (plain-node 最小形实证 crash;vitest/tsx 宿主会吸收,故该面无法用测试钉住,靠此排序保证)。
|
|
898
|
+
// settle 后到达的 'error' 经 finish 的 settled 门自然吞掉。
|
|
660
899
|
child.on("error", (e) => {
|
|
661
900
|
finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) });
|
|
662
901
|
});
|
|
@@ -666,10 +905,14 @@ export class RemoteHostExecutionEnv {
|
|
|
666
905
|
const e = errBuf.result();
|
|
667
906
|
return { stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes) };
|
|
668
907
|
};
|
|
908
|
+
// F4(发版复审):exit 一到即 latch——命令已 completed,此后 timeout/abort 绝不再杀(否则 close
|
|
909
|
+
// 延迟/缺席的 500ms drain 窗内,一个 timeout 会把幸存进程组连坐——恰是本修要消的杀伤面),detach
|
|
910
|
+
// 也不再领养(completed 命令不是后台工作)。只剩 drain timer 与 close 竞争 settle。
|
|
669
911
|
let exited = false;
|
|
670
912
|
const timer = setTimeout(() => {
|
|
671
913
|
if (exited)
|
|
672
|
-
return;
|
|
914
|
+
return; // F4:completed 命令不进杀伤集(drain settle 即将给出正常结果)
|
|
915
|
+
// timeout 仍杀树(RUNNING 命令超时=合法杀伤面;[1116] 修的是 completed 命令被 close 不来拖进杀伤集)
|
|
673
916
|
try {
|
|
674
917
|
if (child.pid != null)
|
|
675
918
|
killTreeHard(child.pid);
|
|
@@ -678,14 +921,14 @@ export class RemoteHostExecutionEnv {
|
|
|
678
921
|
try {
|
|
679
922
|
child.kill("SIGKILL");
|
|
680
923
|
}
|
|
681
|
-
catch { }
|
|
924
|
+
catch { /* noop */ }
|
|
682
925
|
}
|
|
683
926
|
const p = partial();
|
|
684
927
|
finish({ ok: false, error: new ExecutionError("timeout", `host exec exceeded ${timeoutMs}ms; partial stdout: ${p.stdout.slice(-2048)}`) });
|
|
685
928
|
}, timeoutMs);
|
|
686
929
|
const onAbort = () => {
|
|
687
930
|
if (exited)
|
|
688
|
-
return;
|
|
931
|
+
return; // F4 同上:已 completed 的 abort=让 drain settle 正常返回,不杀幸存组
|
|
689
932
|
try {
|
|
690
933
|
if (child.pid != null)
|
|
691
934
|
killTreeHard(child.pid);
|
|
@@ -694,31 +937,43 @@ export class RemoteHostExecutionEnv {
|
|
|
694
937
|
try {
|
|
695
938
|
child.kill("SIGKILL");
|
|
696
939
|
}
|
|
697
|
-
catch { }
|
|
940
|
+
catch { /* noop */ }
|
|
698
941
|
}
|
|
699
942
|
finish({ ok: false, error: new ExecutionError("aborted", "host exec aborted") });
|
|
700
943
|
};
|
|
944
|
+
// F5(发版复审):AbortSignal 不重放 shellReady/workspace 建目录期间已发生的 abort——注册前必查。
|
|
701
945
|
if (options?.abortSignal?.aborted) {
|
|
702
946
|
onAbort();
|
|
703
947
|
return;
|
|
704
948
|
}
|
|
705
949
|
if (options?.abortSignal)
|
|
706
950
|
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
951
|
+
// F1(发版复审):泵先装再挂 detach——already-aborted 的 detachSignal 会同步跑 onDetach→finish,
|
|
952
|
+
// 若泵在其后才装,cleanupSpool 清不到=幽灵泵继续 truncate 已移交的文件+常驻泄漏。
|
|
707
953
|
pump = setInterval(pumpOnce, PUMP_MS);
|
|
708
954
|
pump.unref?.();
|
|
709
955
|
const onDetach = () => {
|
|
710
956
|
if (settled || exited)
|
|
711
|
-
return;
|
|
712
|
-
pumpOnce();
|
|
957
|
+
return; // F4:completed 命令不领养(它不是后台工作;drain settle 在途)
|
|
958
|
+
pumpOnce(); // fg 段读平(exec 结果里的 seed 完整)
|
|
959
|
+
// handover 游标=0(消费方契约,旧 mem 形同语义):core 的 Bash 工具在 detach 时只给模型
|
|
960
|
+
// 「moved to background」marker、丢弃 exec 结果的 stdout——完整历史必须活在 bg spool 里由
|
|
961
|
+
// BashOutput 给。spool FILE 形从 0 重读同一文件=fg+post 单一字节流零接缝零 U+FFFD(mem-tee
|
|
962
|
+
// 为此专门修过的病结构性不存在);已被轮转 truncate 的头部(>8MB)在 exec 结果里已 markTruncated。
|
|
963
|
+
// F2(发版复审):fg 期发生过轮转/skip 的丢失字节随 handover 记账(bg 首拍 poll 并入
|
|
964
|
+
// bytesDroppedBeforeCursor → BashOutput 的 truncated 面诚实),从 0 读只覆盖现存代。
|
|
965
|
+
// F2(二轮复审):handover 丢失=从命名 spool 文件真正移除的字节(spoolRemoved),不是 RollingTail
|
|
966
|
+
// 尾窗的 head-evict 计数——后者在轮转 truncate 后仍报 0(尾窗满),会漏计整代。
|
|
713
967
|
const fgDropped = spoolRemoved.out + spoolRemoved.err;
|
|
714
968
|
const shellId = this.adoptRunningExecSpool(child, spool, { out: 0, err: 0 }, fgDropped);
|
|
715
969
|
if (shellId === undefined)
|
|
716
|
-
return;
|
|
717
|
-
handedOver = true;
|
|
970
|
+
return; // 拒收(limit/无 pid)→ 继续 fg,泵照跑,close 正常 settle
|
|
971
|
+
handedOver = true; // spool 文件归 bg(dispose 面按 FILE 形清理)
|
|
972
|
+
// cut-kill registry 注销(所有权已移交 bgManager;pipe 径同位)。
|
|
718
973
|
try {
|
|
719
974
|
options?.onDetachAdopted?.();
|
|
720
975
|
}
|
|
721
|
-
catch { }
|
|
976
|
+
catch { /* noop */ }
|
|
722
977
|
const o = outBuf.result();
|
|
723
978
|
const e = errBuf.result();
|
|
724
979
|
finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode: 0, detached: { shellId } }));
|
|
@@ -740,6 +995,7 @@ export class RemoteHostExecutionEnv {
|
|
|
740
995
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
|
|
741
996
|
finish(ok({ stdout: markTruncated(o.text, o.droppedBytes), stderr: markTruncated(e.text, e.droppedBytes), exitCode }));
|
|
742
997
|
};
|
|
998
|
+
// ('error' 监听已在 spawn 后立即装——七轮复审:F5 提前 return 前必须有监听,防 uncaught)
|
|
743
999
|
child.on("close", (code, signal) => {
|
|
744
1000
|
if (drainTimer) {
|
|
745
1001
|
clearTimeout(drainTimer);
|
|
@@ -748,12 +1004,18 @@ export class RemoteHostExecutionEnv {
|
|
|
748
1004
|
settleFromTermination(code, signal);
|
|
749
1005
|
});
|
|
750
1006
|
child.on("exit", (code, signal) => {
|
|
1007
|
+
// F4(二轮复审):exit **立即** latch——命令已 completed,此后 timeout/abort/detach 全 no-op(否则
|
|
1008
|
+
// close 延迟/缺席时,drain 窗内的 timeout 会 killTree 连坐幸存孙进程;一轮修漏了这行赋值)。
|
|
751
1009
|
exited = true;
|
|
1010
|
+
// [1116] 病理根除点:exit=子 shell 已终——孙进程(nohup 服务)可以继续持有 spool 文件 fd(写文件
|
|
1011
|
+
// 不牵连本 exec 的 settle),500ms drain 后兜底 settle;close 先来则上面清掉本 timer 直接 settle。
|
|
752
1012
|
drainTimer = setTimeout(() => settleFromTermination(code, signal), EXIT_SETTLE_DRAIN_MS);
|
|
753
1013
|
drainTimer.unref?.();
|
|
754
1014
|
});
|
|
755
1015
|
});
|
|
756
1016
|
}
|
|
1017
|
+
/** [1116] spool 形 detach adoption:spool 路径+fg 游标直接进 HostBgState 的 FILE 面(launch 同形,poll
|
|
1018
|
+
* 共码);同一文件单一字节流=天然无 decoder 接缝(mem-tee 版为此专门修过的病在此结构性不存在)。 */
|
|
757
1019
|
adoptRunningExecSpool(child, spool, cursors, fgRemovedBytes = 0) {
|
|
758
1020
|
const pid = child.pid;
|
|
759
1021
|
if (pid == null)
|
|
@@ -774,12 +1036,16 @@ export class RemoteHostExecutionEnv {
|
|
|
774
1036
|
});
|
|
775
1037
|
return adopted?.shellId;
|
|
776
1038
|
}
|
|
1039
|
+
// ─────────────────────────────── streaming exec ───────────────────────────────
|
|
777
1040
|
async *execStream(command, options) {
|
|
778
1041
|
const ready = await this.shellReady();
|
|
779
1042
|
if (!ready.ok)
|
|
780
1043
|
throw new RemoteExecutionError("connect_failed", ready.error.message, ready.error);
|
|
781
1044
|
const cwd = this.resolve(options?.cwd ?? this.cwd);
|
|
782
1045
|
const timeoutMs = options?.timeout != null ? options.timeout * 1000 : undefined;
|
|
1046
|
+
// Liveness is opt-in here (unlike e2b's default-on idle cap). Deliberate divergence:
|
|
1047
|
+
// host runs on the operator's own machine — no provider-unreachability hang class — and a missing
|
|
1048
|
+
// readTimeoutMs means the caller's own AbortSignal is the abort path. So no default wall-clock cap.
|
|
783
1049
|
const idleMs = options?.readTimeoutMs;
|
|
784
1050
|
const cap = options?.maxOutputBytes;
|
|
785
1051
|
const queue = [];
|
|
@@ -790,7 +1056,7 @@ export class RemoteHostExecutionEnv {
|
|
|
790
1056
|
let lastChunk = Date.now();
|
|
791
1057
|
let wake;
|
|
792
1058
|
const signalReady = () => { const w = wake; wake = undefined; w?.(); };
|
|
793
|
-
const sh = hostShell();
|
|
1059
|
+
const sh = hostShell(); // Windows S1/S3: platform-resolved shell + group options (POSIX byte-identical)
|
|
794
1060
|
const child = spawn(sh.shell, [...sh.args, command], { cwd, env: this.mergeEnv(options?.env), ...spawnGroupOptions() });
|
|
795
1061
|
let pipeDestroyTimer;
|
|
796
1062
|
const kill = () => {
|
|
@@ -802,8 +1068,12 @@ export class RemoteHostExecutionEnv {
|
|
|
802
1068
|
try {
|
|
803
1069
|
child.kill("SIGKILL");
|
|
804
1070
|
}
|
|
805
|
-
catch { }
|
|
1071
|
+
catch { /* noop */ }
|
|
806
1072
|
}
|
|
1073
|
+
// S2b: same D5 pipe-destroy grace the buffered exec has — a kill-escaping process (POSIX
|
|
1074
|
+
// re-parented grandchild; win32 taskkill out-of-tree escapee) holding our pipes means `close` never
|
|
1075
|
+
// fires and the stream hangs on its own idle/abort path forever. Force-free the FDs after the grace so
|
|
1076
|
+
// the generator's close-driven finish always arrives. Idempotent (one timer; cleared on close).
|
|
807
1077
|
pipeDestroyTimer ??= armPipeDestroyGrace(child);
|
|
808
1078
|
};
|
|
809
1079
|
const enqueue = (type, data) => {
|
|
@@ -825,7 +1095,7 @@ export class RemoteHostExecutionEnv {
|
|
|
825
1095
|
});
|
|
826
1096
|
child.on("close", (code, signal) => {
|
|
827
1097
|
if (pipeDestroyTimer)
|
|
828
|
-
clearTimeout(pipeDestroyTimer);
|
|
1098
|
+
clearTimeout(pipeDestroyTimer); // pipes closed naturally → the S2b grace is moot
|
|
829
1099
|
if (finished)
|
|
830
1100
|
return;
|
|
831
1101
|
exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
|
|
@@ -846,6 +1116,7 @@ export class RemoteHostExecutionEnv {
|
|
|
846
1116
|
else
|
|
847
1117
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
848
1118
|
}
|
|
1119
|
+
// Optional total wall-clock cap (host: only when the caller asks).
|
|
849
1120
|
const wallTimer = timeoutMs != null
|
|
850
1121
|
? setTimeout(() => {
|
|
851
1122
|
if (finished)
|
|
@@ -872,6 +1143,7 @@ export class RemoteHostExecutionEnv {
|
|
|
872
1143
|
throw failure;
|
|
873
1144
|
if (finished)
|
|
874
1145
|
break;
|
|
1146
|
+
// idle/liveness bound: no chunk within readTimeoutMs → typed timeout (a hung command).
|
|
875
1147
|
const waitMs = idleMs != null ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
|
|
876
1148
|
await new Promise((r) => { wake = r; setTimeout(r, waitMs); });
|
|
877
1149
|
if (idleMs != null && !finished && Date.now() - lastChunk >= idleMs) {
|
|
@@ -890,12 +1162,13 @@ export class RemoteHostExecutionEnv {
|
|
|
890
1162
|
if (!finished) {
|
|
891
1163
|
finished = true;
|
|
892
1164
|
kill();
|
|
893
|
-
}
|
|
1165
|
+
} // consumer abandoned early → don't leak the child
|
|
894
1166
|
if (wallTimer)
|
|
895
1167
|
clearTimeout(wallTimer);
|
|
896
1168
|
options?.signal?.removeEventListener("abort", onAbort);
|
|
897
1169
|
}
|
|
898
1170
|
}
|
|
1171
|
+
// ─────────────────────────────── FileSystem ───────────────────────────────
|
|
899
1172
|
async absolutePath(p) {
|
|
900
1173
|
return ok(this.resolve(p));
|
|
901
1174
|
}
|
|
@@ -944,6 +1217,7 @@ export class RemoteHostExecutionEnv {
|
|
|
944
1217
|
return ready;
|
|
945
1218
|
const abs = this.resolve(p);
|
|
946
1219
|
try {
|
|
1220
|
+
// Create parent dirs automatically (parity with the E2B adapter).
|
|
947
1221
|
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
948
1222
|
const data = typeof content === "string" ? content : Buffer.from(content);
|
|
949
1223
|
await fs.writeFile(abs, data, { signal: abortSignal });
|
|
@@ -954,6 +1228,9 @@ export class RemoteHostExecutionEnv {
|
|
|
954
1228
|
}
|
|
955
1229
|
}
|
|
956
1230
|
async appendFile(p, content, abortSignal) {
|
|
1231
|
+
// `lingeringBgWrite`:destroy 之后**仍在册**的后台 shell 还要把输出镜像到文件 —— 见 fsReady 旁注。
|
|
1232
|
+
// 只有 append 开这个口:core 的后台输出镜像唯一走 `env.appendFile(handle.outputFile, …)`,别的文件动作
|
|
1233
|
+
// 在 destroy 之后没有正当理由(读面留给下一轮的新 env)。
|
|
957
1234
|
const ready = await this.fsReady(p, { lingeringBgWrite: true });
|
|
958
1235
|
if (!ready.ok)
|
|
959
1236
|
return ready;
|
|
@@ -976,7 +1253,7 @@ export class RemoteHostExecutionEnv {
|
|
|
976
1253
|
return ready;
|
|
977
1254
|
const abs = this.resolve(p);
|
|
978
1255
|
try {
|
|
979
|
-
const st = await fs.lstat(abs);
|
|
1256
|
+
const st = await fs.lstat(abs); // no follow (FileInfo contract)
|
|
980
1257
|
return ok({
|
|
981
1258
|
name: path.basename(abs),
|
|
982
1259
|
path: abs,
|
|
@@ -1007,6 +1284,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1007
1284
|
mtimeMs = st.mtimeMs;
|
|
1008
1285
|
}
|
|
1009
1286
|
catch {
|
|
1287
|
+
/* a child that vanished between readdir and lstat → keep kind from Dirent, size/mtime 0 */
|
|
1010
1288
|
}
|
|
1011
1289
|
out.push({ name: e.name, path: child, kind: kindFromDirent(e), size, mtimeMs });
|
|
1012
1290
|
}
|
|
@@ -1017,6 +1295,10 @@ export class RemoteHostExecutionEnv {
|
|
|
1017
1295
|
}
|
|
1018
1296
|
}
|
|
1019
1297
|
async canonicalPath(p, abortSignal) {
|
|
1298
|
+
// 🔴 只读面也过 fsReady(2026-07-25 统一):`exists`/`canonicalPath`/`readLink` 此前**不查** destroyed,
|
|
1299
|
+
// 于是拆除之后「exists 答 true、紧接着 readTextFile 答 already destroyed」这一对自相矛盾 —— 调用方按
|
|
1300
|
+
// exists 的结果去读就撞一个措辞完全不同的错。同批把 adb 腿的读面也补上了门(它此前连 connected 都不查),
|
|
1301
|
+
// 这里是同一件事:**同族一致性**。只读探测本身无害,但"有害/无害"不该是各方法自行其是的判断。
|
|
1020
1302
|
const ready = await this.fsReady(p);
|
|
1021
1303
|
if (!ready.ok)
|
|
1022
1304
|
return ready;
|
|
@@ -1029,6 +1311,10 @@ export class RemoteHostExecutionEnv {
|
|
|
1029
1311
|
}
|
|
1030
1312
|
}
|
|
1031
1313
|
async readLink(p, abortSignal) {
|
|
1314
|
+
// 🔴 只读面也过 fsReady(2026-07-25 统一):`exists`/`canonicalPath`/`readLink` 此前**不查** destroyed,
|
|
1315
|
+
// 于是拆除之后「exists 答 true、紧接着 readTextFile 答 already destroyed」这一对自相矛盾 —— 调用方按
|
|
1316
|
+
// exists 的结果去读就撞一个措辞完全不同的错。同批把 adb 腿的读面也补上了门(它此前连 connected 都不查),
|
|
1317
|
+
// 这里是同一件事:**同族一致性**。只读探测本身无害,但"有害/无害"不该是各方法自行其是的判断。
|
|
1032
1318
|
const ready = await this.fsReady(p);
|
|
1033
1319
|
if (!ready.ok)
|
|
1034
1320
|
return ready;
|
|
@@ -1041,6 +1327,10 @@ export class RemoteHostExecutionEnv {
|
|
|
1041
1327
|
}
|
|
1042
1328
|
}
|
|
1043
1329
|
async exists(p, abortSignal) {
|
|
1330
|
+
// 🔴 只读面也过 fsReady(2026-07-25 统一):`exists`/`canonicalPath`/`readLink` 此前**不查** destroyed,
|
|
1331
|
+
// 于是拆除之后「exists 答 true、紧接着 readTextFile 答 already destroyed」这一对自相矛盾 —— 调用方按
|
|
1332
|
+
// exists 的结果去读就撞一个措辞完全不同的错。同批把 adb 腿的读面也补上了门(它此前连 connected 都不查),
|
|
1333
|
+
// 这里是同一件事:**同族一致性**。只读探测本身无害,但"有害/无害"不该是各方法自行其是的判断。
|
|
1044
1334
|
const ready = await this.fsReady(p);
|
|
1045
1335
|
if (!ready.ok)
|
|
1046
1336
|
return ready;
|
|
@@ -1052,7 +1342,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1052
1342
|
catch (e) {
|
|
1053
1343
|
const fe = this.fileError(e, abs, abortSignal);
|
|
1054
1344
|
if (fe.code === "not_found")
|
|
1055
|
-
return ok(false);
|
|
1345
|
+
return ok(false); // missing path is `false`, not an error (contract)
|
|
1056
1346
|
return { ok: false, error: fe };
|
|
1057
1347
|
}
|
|
1058
1348
|
}
|
|
@@ -1070,12 +1360,13 @@ export class RemoteHostExecutionEnv {
|
|
|
1070
1360
|
}
|
|
1071
1361
|
}
|
|
1072
1362
|
async remove(p, options) {
|
|
1073
|
-
const ready = await this.fsReady(p);
|
|
1363
|
+
const ready = await this.fsReady(p); // mutation: fail closed after destroy() — parity with writeFile/createDir + e2b/ssh
|
|
1074
1364
|
if (!ready.ok)
|
|
1075
1365
|
return ready;
|
|
1076
1366
|
const abs = this.resolve(p);
|
|
1077
1367
|
try {
|
|
1078
1368
|
if (!options?.force) {
|
|
1369
|
+
// Surface a missing path as not_found (fs.rm with force:false already throws ENOENT, but be explicit).
|
|
1079
1370
|
await fs.lstat(abs);
|
|
1080
1371
|
}
|
|
1081
1372
|
await fs.rm(abs, { recursive: options?.recursive ?? false, force: options?.force ?? false });
|
|
@@ -1088,6 +1379,18 @@ export class RemoteHostExecutionEnv {
|
|
|
1088
1379
|
return { ok: false, error: fe };
|
|
1089
1380
|
}
|
|
1090
1381
|
}
|
|
1382
|
+
/** 🔴 临时文件/目录**不放进 workspace**(2026-07-25 修)。
|
|
1383
|
+
* 原来两者都建在 `this.workspaceDir` 下,而 **TOC/CC 形的 workspaceDir 就是用户自己的目录**,且 `destroy()`
|
|
1384
|
+
* 明文永不删它 ⇒ core 给**每个**后台 bash 都调一次 `createTempFile({prefix:"bash-task-", suffix:".output"})`
|
|
1385
|
+
* ⇒ 用户仓库根目录按后台任务数累积 `bash-task-<hex>.output`(内容=命令输出)+ 每次 `createTempDir` 一个
|
|
1386
|
+
* `tmp-XXXXXX/`,**永久**留在他们的 git 树里。两条独立判据说明这是偏差不是设计:①core 自己的 TOC 参照实现
|
|
1387
|
+
* `NodeExecutionEnv.createTempFile` 放的是 `os.tmpdir()`;②**本适配器早就为了同一个理由把 spool 根搬去了
|
|
1388
|
+
* tmpdir**([1116] 五轮复审明写"不再弄脏 user-owned workspace"),只是 createTempFile/Dir 没跟上。
|
|
1389
|
+
* 改用本 env 自己的 tmp 根(`ensureSpoolRoot()`,已在 `os.tmpdir()` 下):
|
|
1390
|
+
* · 用户目录不再被写入;
|
|
1391
|
+
* · 镜像写仍然可用 —— `isLingeringMirrorTarget` 同批放行这个根(见那里的旁注);
|
|
1392
|
+
* · ephemeral 形反而更好:`destroy()` 在**有后台 shell 在册**时不删这个根,而 workspace 是会被 rm 的。
|
|
1393
|
+
* ⚠️ 这是 **host-only**:k8s/local-docker 的临时文件必须落在**容器内**可见的 cwd,ssh/e2b 本来就用远端系统 tmp。 */
|
|
1091
1394
|
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
1092
1395
|
const ready = await this.fsReady(".");
|
|
1093
1396
|
if (!ready.ok)
|
|
@@ -1107,23 +1410,29 @@ export class RemoteHostExecutionEnv {
|
|
|
1107
1410
|
const suffix = options?.suffix ?? "";
|
|
1108
1411
|
try {
|
|
1109
1412
|
const name = `${prefix}${randomBytes(8).toString("hex")}${suffix}`;
|
|
1110
|
-
const abs = path.join(this.ensureSpoolRoot(), name);
|
|
1413
|
+
const abs = path.join(this.ensureSpoolRoot(), name); // 见 createTempDir 上方旁注:不放进用户的 workspace
|
|
1111
1414
|
if (options?.abortSignal?.aborted)
|
|
1112
1415
|
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
1113
|
-
await fs.writeFile(abs, "", { flag: "wx" });
|
|
1416
|
+
await fs.writeFile(abs, "", { flag: "wx" }); // wx = fail if exists (random → won't)
|
|
1114
1417
|
return ok(abs);
|
|
1115
1418
|
}
|
|
1116
1419
|
catch (e) {
|
|
1117
1420
|
return { ok: false, error: this.fileError(e, this.workspaceDir, options?.abortSignal) };
|
|
1118
1421
|
}
|
|
1119
1422
|
}
|
|
1423
|
+
// ─────────────────────────────── internals ───────────────────────────────
|
|
1424
|
+
/** Resolve a (possibly relative) path against {@link cwd} like the other adapters. Host paths are platform-native. */
|
|
1120
1425
|
resolve(p) {
|
|
1121
1426
|
return path.isAbsolute(p) ? path.normalize(p) : path.normalize(path.resolve(this.cwd, p));
|
|
1122
1427
|
}
|
|
1428
|
+
/** Lazily create the per-task workspace exactly once (memoized; concurrent first-use shares it). */
|
|
1123
1429
|
ensurePrepared() {
|
|
1124
1430
|
if (this.prepared)
|
|
1125
1431
|
return Promise.resolve();
|
|
1126
1432
|
if (!this.preparing) {
|
|
1433
|
+
// Reset the memo on rejection so a later call can re-attempt after a transient mkdir
|
|
1434
|
+
// failure (ENOSPC / racy EACCES / rm race) — mirrors e2b's reset-on-failed-provision.
|
|
1435
|
+
// Re-throw (don't swallow) so shellReady/fsReady/connect still surface the original error.
|
|
1127
1436
|
this.preparing = fs
|
|
1128
1437
|
.mkdir(this.workspaceDir, { recursive: true })
|
|
1129
1438
|
.then(() => {
|
|
@@ -1140,6 +1449,11 @@ export class RemoteHostExecutionEnv {
|
|
|
1140
1449
|
if (this.destroyed)
|
|
1141
1450
|
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
1142
1451
|
try {
|
|
1452
|
+
// Windows S1 (design D1): resolve the host shell ONCE (POSIX = /bin/sh, sync constant; win32 = Git Bash
|
|
1453
|
+
// via core getShellConfig, fail-loud when absent). Every spawn site is behind this gate, so the sync
|
|
1454
|
+
// `hostShell()` reads below are always post-resolution. A missing Git Bash surfaces as its own
|
|
1455
|
+
// shell_unavailable error with the install hint — never a silent cmd fallback, never mislabeled as a
|
|
1456
|
+
// workspace failure.
|
|
1143
1457
|
try {
|
|
1144
1458
|
await resolveHostShell();
|
|
1145
1459
|
}
|
|
@@ -1153,10 +1467,48 @@ export class RemoteHostExecutionEnv {
|
|
|
1153
1467
|
return { ok: false, error: new ExecutionError("shell_unavailable", `failed to create host workspace: ${errMsg(e)}`, asError(e)) };
|
|
1154
1468
|
}
|
|
1155
1469
|
}
|
|
1470
|
+
/**
|
|
1471
|
+
* @param opts.lingeringBgWrite 只由 {@link appendFile} 传 true —— 允许「destroy 之后、仍在册的后台 shell
|
|
1472
|
+
* 的输出镜像写」。
|
|
1473
|
+
*
|
|
1474
|
+
* 🔴 为什么需要这个口(2026-07-25,cli dogfood 报的现象追出来的真因):`destroy()` 无条件置
|
|
1475
|
+
* `destroyed = true`,于是文件面**全拒**;而 `pollBackground` 直通 bgManager、**没有** destroyed 门。
|
|
1476
|
+
* 这与本适配器自己遵守的 core 契约(1.258.2「destroy 不得收割后台进程」,上方 destroy() 里也为
|
|
1477
|
+
* 「dispose 前仍可轮询」的残余刻意保留了 spool 根)直接打架:我们**刻意**让后台 shell 跨 destroy 存活并
|
|
1478
|
+
* 继续被轮询,却拒掉它们要写的那个文件 ⇒ core 的 `appendFile` 一失败即置 `spool.mirrorFailed`,用户在一个
|
|
1479
|
+
* 其实成功的后台任务上看到「[!] The output file is INCOMPLETE」。已用真子进程执行验证过这个不对称。
|
|
1480
|
+
*
|
|
1481
|
+
* 放行判据刻意收紧到三条同时成立,少一条都拒:
|
|
1482
|
+
* ① `persistent` —— 只有 TOC/CC 形(构造时给了 `workspaceDir` = 用户自己的目录)。那一形 destroy() 明文
|
|
1483
|
+
* 「NEVER rm -rf it」,什么都没删,文件确实还在,core 还告诉用户「Read it any time」⇒ 报 INCOMPLETE 是谎。
|
|
1484
|
+
* **ephemeral 形不放行**:它的 destroy() 真的 rm -rf 了 workspace,而 `createTempFile` 恰把 outputFile
|
|
1485
|
+
* 建在 workspace 里 ⇒ 镜像目标已被删,拒写是**诚实**的;放行反而会让 appendFile 的 mkdir -p 把一个已拆除
|
|
1486
|
+
* 的工作目录**复活**成孤儿目录。
|
|
1487
|
+
* ② 还有后台 shell **在册** —— 判据与 destroy() 里"跳过删 spool 根"用的是同一个 `hasShells()`,即
|
|
1488
|
+
* 「还有人在读写这些文件」这一个事实,不是两套规则。没有 shell 在册时 `destroyed` 照旧是硬护栏。
|
|
1489
|
+
* ③ 只对 append 一个动作开 —— 不是把整个文件面在 destroy 之后重新开门。
|
|
1490
|
+
* 用 `this._bgManager?.` 而不是惰性 getter:拆除路径上不该顺手 **新建** 一个 manager。
|
|
1491
|
+
*/
|
|
1492
|
+
/** destroy 后「残余镜像写」的**路径**判据 —— 与方法判据(`lingeringBgWrite`)并列,两者都满足才放行。
|
|
1493
|
+
*
|
|
1494
|
+
* 🔴 为什么需要它(深挖第一轮打在我自己头上的一条,2026-07-25):我原先的放行只看方法标志,于是 destroy 之后
|
|
1495
|
+
* `appendFile` 能**新建**文件、`mkdir -p` 建整棵树、甚至写 workspace **之外**的绝对路径 —— 而我在 fsReady
|
|
1496
|
+
* 头注里写的是「别的文件动作在 destroy 之后没有正当理由」。探针实测把这个不一致钉了出来:说是"窄",在路径
|
|
1497
|
+
* 维度上一点都不窄。
|
|
1498
|
+
*
|
|
1499
|
+
* 收紧到恰好贴合唯一的正当调用方(core 的后台输出镜像):
|
|
1500
|
+
* ① 目标必须在 **workspace 之内**(`createTempFile` 就把 outputFile 建在这儿)—— 挡住"写到工作区外面";
|
|
1501
|
+
* ② 目标**必须已经存在** —— 镜像写的文件是 destroy **之前**建好的,所以"新建"从来不是这条路的需求;
|
|
1502
|
+
* 这一条同时挡掉 `mkdir -p` 建新目录树的副作用。
|
|
1503
|
+
* 同步 `existsSync` 而不是 await:这是拆除路径上的判据,不该再引入一次异步让出。 */
|
|
1156
1504
|
isLingeringMirrorTarget(p) {
|
|
1157
1505
|
const abs = this.resolve(p);
|
|
1158
1506
|
if (!existsSync(abs))
|
|
1159
|
-
return false;
|
|
1507
|
+
return false; // 必须是既存文件(镜像目标在 destroy 前就已建好)
|
|
1508
|
+
// 🔴 必须在**同一路径空间**里比 containment:core 传进来的是 `canonicalPath()` 的产物(realpath),
|
|
1509
|
+
// 而 `workspaceDir` 是构造时给的原样串 —— macOS 上 `/var/...` 与 `/private/var/...` 是同一目录的两种写法,
|
|
1510
|
+
// 直接 `path.relative` 会把合法目标判成"在 workspace 之外"。(这条是我自己的行为面测试当场按住的:
|
|
1511
|
+
// 收紧判据的第一版让 `host-env-bg-mirror-after-destroy` 立刻红。)两侧都过 realpath 再比。
|
|
1160
1512
|
const real = (x) => { try {
|
|
1161
1513
|
return realpathSync(x);
|
|
1162
1514
|
}
|
|
@@ -1167,13 +1519,23 @@ export class RemoteHostExecutionEnv {
|
|
|
1167
1519
|
const rel = path.relative(real(root), real(abs));
|
|
1168
1520
|
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
1169
1521
|
};
|
|
1522
|
+
// workspace **或**本 env 自己的 tmp 根:后者是 `createTempFile` 现在的落点(同批把临时文件搬出用户目录,
|
|
1523
|
+
// 见 createTempDir 上方旁注)。两个根都是"本 env 自有"的面,放行它们不会让残余写触及无关路径。
|
|
1524
|
+
// 用 `this.spoolRootDir` 而不是 `ensureSpoolRoot()`:拆除路径上不该顺手**新建**一个 tmp 根。
|
|
1170
1525
|
return inside(this.workspaceDir) || (this.spoolRootDir !== undefined && inside(this.spoolRootDir));
|
|
1171
1526
|
}
|
|
1172
1527
|
async fsReady(p, opts) {
|
|
1173
1528
|
if (this.destroyed) {
|
|
1529
|
+
// 🔴 判据从三条收成两条 +路径判据(2026-07-25 同批):原先还要求 `this.persistent`,理由是
|
|
1530
|
+
// 「ephemeral 形的 workspace 会被 rm,放行会让 mkdir -p **复活**一个已拆除的目录」。同批把临时文件搬到本 env
|
|
1531
|
+
// 的 tmp 根之后,那个理由**已被 `isLingeringMirrorTarget` 里的 `existsSync` 覆盖**:workspace 被删 ⇒ 目标不
|
|
1532
|
+
// 存在 ⇒ 自然拒;而"必须是既存文件"也让 mkdir -p 无从复活任何东西。继续要求 `persistent` 只会挡住
|
|
1533
|
+
// ephemeral 形**本该成功**的镜像写(目标在 tmp 根、destroy 在有 shell 在册时不删它)。
|
|
1174
1534
|
const lingering = opts?.lingeringBgWrite === true && this._bgManager?.hasShells() === true && this.isLingeringMirrorTarget(p);
|
|
1175
1535
|
if (!lingering)
|
|
1176
1536
|
return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
|
|
1537
|
+
// 放行:不再走 ensurePrepared() —— destroy 已清空 preparing memo,而 persistent 形的目录本来就是用户的、
|
|
1538
|
+
// 必然存在(后台 shell 正在里面跑),且 appendFile 自己还会 mkdir -p 父目录。
|
|
1177
1539
|
return ok(undefined);
|
|
1178
1540
|
}
|
|
1179
1541
|
try {
|
|
@@ -1184,6 +1546,10 @@ export class RemoteHostExecutionEnv {
|
|
|
1184
1546
|
return { ok: false, error: new FileError("unknown", `failed to create host workspace: ${errMsg(e)}`, this.resolve(p), asError(e)) };
|
|
1185
1547
|
}
|
|
1186
1548
|
}
|
|
1549
|
+
/** Base env (process.env when inheriting) merged with the adapter's secret env, then per-command overrides win. */
|
|
1550
|
+
/** D1 secure default = `"scrub"`: inherit `process.env` minus secret-pattern keys (core's `scrubSecretEnv`, single
|
|
1551
|
+
* source of truth). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
|
|
1552
|
+
* maps `true`→`"all"` / `false`→`[]`. */
|
|
1187
1553
|
inheritMode() {
|
|
1188
1554
|
if (this.cfg.inheritEnv !== undefined)
|
|
1189
1555
|
return this.cfg.inheritEnv;
|
|
@@ -1205,11 +1571,16 @@ export class RemoteHostExecutionEnv {
|
|
|
1205
1571
|
base[k] = process.env[k];
|
|
1206
1572
|
}
|
|
1207
1573
|
else
|
|
1208
|
-
base = scrubSecretEnv(process.env);
|
|
1574
|
+
base = scrubSecretEnv(process.env); // "scrub" — core's first-party D1 secret-env filter
|
|
1209
1575
|
if (this.cfg.env)
|
|
1210
|
-
Object.assign(base, this.cfg.env);
|
|
1576
|
+
Object.assign(base, this.cfg.env); // caller-supplied (control-plane secrets) verbatim
|
|
1211
1577
|
if (perCommand)
|
|
1212
|
-
Object.assign(base, perCommand);
|
|
1578
|
+
Object.assign(base, perCommand); // per-call wins
|
|
1579
|
+
// Windows S2b (design D1): win32 env keys are case-insensitive — a case-sensitive merge that
|
|
1580
|
+
// produced BOTH `Path` (inherited) and `PATH` (override) reaches CreateProcess as one-undefined-which.
|
|
1581
|
+
// Pin SHELL to the resolved Git Bash (subshell scripts read $SHELL) BEFORE the collapse (so an inherited
|
|
1582
|
+
// case-variant `Shell` folds into one entry), then collapse case-duplicates (later = higher-precedence
|
|
1583
|
+
// value, first-seen = native casing). POSIX: both are identity/no-ops — byte-identical.
|
|
1213
1584
|
if (process.platform === "win32")
|
|
1214
1585
|
base.SHELL = hostShell().shell;
|
|
1215
1586
|
return collapseWin32EnvKeys(base);
|
|
@@ -1235,6 +1606,7 @@ export class RemoteHostExecutionEnv {
|
|
|
1235
1606
|
return new FileError("unknown", msg, p);
|
|
1236
1607
|
}
|
|
1237
1608
|
}
|
|
1609
|
+
/** Strip anything but [A-Za-z0-9._-] from an id so it is safe in a directory name. */
|
|
1238
1610
|
function sanitizeId(id) {
|
|
1239
1611
|
return id.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "task";
|
|
1240
1612
|
}
|
|
@@ -1251,16 +1623,42 @@ function errMsg(e) {
|
|
|
1251
1623
|
function asError(e) {
|
|
1252
1624
|
return e instanceof Error ? e : undefined;
|
|
1253
1625
|
}
|
|
1626
|
+
/** Conventional exit code for a process killed by a signal (128 + signal number); falls back to undefined.
|
|
1627
|
+
* Uses Node's authoritative platform table (`os.constants.signals`) — a hand-written table missed SIGABRT/SIGPIPE
|
|
1628
|
+
* (`kill -ABRT` reported 137 instead of 134, impersonating SIGKILL). Callers keep the `?? 9`
|
|
1629
|
+
* fallback for a name the platform doesn't define, matching core's `SIGNUM[signal] ?? 9`. */
|
|
1254
1630
|
function signalNumber(signal) {
|
|
1255
1631
|
return os.constants.signals[signal];
|
|
1256
1632
|
}
|
|
1633
|
+
/**
|
|
1634
|
+
* `ExecutionEnvFactory` for the TOC `host` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment makes
|
|
1635
|
+
* its agent run DIRECTLY on the worker's machine (no container) — the local-coding-agent default. One fresh
|
|
1636
|
+
* env per task; the Runner owns the lifetime and calls `destroy()` (= `rm -rf` the workspace) on task end.
|
|
1637
|
+
*
|
|
1638
|
+
* Mirrors `e2bExecutionEnvFactory`: returns the env **unconnected** (lazy) and folds the per-task
|
|
1639
|
+
* `ctx.sessionId` into the workspace dir id so two concurrent tasks never share a working directory.
|
|
1640
|
+
*/
|
|
1257
1641
|
export function hostExecutionEnvFactory(config = {}) {
|
|
1258
1642
|
return (ctx) => new RemoteHostExecutionEnv({
|
|
1259
1643
|
...config,
|
|
1260
1644
|
id: config.id ?? ctx.taskId ?? ctx.sessionId,
|
|
1261
1645
|
});
|
|
1262
1646
|
}
|
|
1647
|
+
// ─────────────────────────────── remoteExec superset: docker providers ───────────────────────────────
|
|
1648
|
+
//
|
|
1649
|
+
// DUAL-MODE-DESIGN §5 defines a `remoteExec.provider` superset. `host` (above) is implemented; `local-docker`
|
|
1650
|
+
// is NOW IMPLEMENTED (the isolation lane) — it lives in its own module (remote-env-local-docker.ts) and is
|
|
1651
|
+
// re-exported here so the discriminator entry point + the historical import site stay stable. `remote-docker`
|
|
1652
|
+
// (a REMOTE docker daemon over TLS/SSH) is still an explicit fast-follow stub — it throws a clear "not yet
|
|
1653
|
+
// implemented" so selecting it fails loud at wiring time instead of silently degrading isolation. NOT faked green.
|
|
1654
|
+
// `local-docker` (the isolation lane): real implementation moved to remote-env-local-docker.ts. Re-export the
|
|
1655
|
+
// factory + config from here so existing wiring (`import { localDockerExecutionEnvFactory } from "./remote-env-host"`)
|
|
1656
|
+
// keeps working and the provider discriminator has one canonical entry point.
|
|
1263
1657
|
export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
1658
|
+
/**
|
|
1659
|
+
* `remote-docker` factory entry point — wired but an explicit TODO (DUAL-MODE-DESIGN §5 fast-follow). Throws
|
|
1660
|
+
* at factory-BUILD time so selecting it before it exists fails loud rather than degrading isolation silently.
|
|
1661
|
+
*/
|
|
1264
1662
|
export function remoteDockerExecutionEnvFactory(_config) {
|
|
1265
1663
|
throw new RemoteExecutionError("unsupported", "remoteExec provider 'remote-docker' is not yet implemented (DUAL-MODE-DESIGN §5 fast-follow) — use 'host', 'local-docker', or 'e2b' until it ships");
|
|
1266
1664
|
}
|