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