@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,42 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `RemoteLocalDockerExecutionEnv` — the TOC **local-docker** exec adapter (DUAL-MODE-DESIGN §5, the
|
|
3
|
-
* fast-follow promoted from the host file's typed stub). Runs the agent's bash/file tools inside a long-lived
|
|
4
|
-
* container ON THIS MACHINE's docker daemon, so a single-machine ("take-over-this-computer") deployment gets a
|
|
5
|
-
* REAL OS-level isolation boundary — the in-between lane between the un-isolated `host` adapter (no container)
|
|
6
|
-
* and the fleet `e2b`/`k8s` sandboxes (remote microVMs/pods).
|
|
7
|
-
*
|
|
8
|
-
* It is a PEER adapter to host / e2b / k8s / ssh / adb (NOT chained): all implement the SAME core seam
|
|
9
|
-
* `RemoteExecutionEnv` (core src/core/remote-env.ts, design/48 §5), selected by `remoteExec.provider` —
|
|
10
|
-
* this one is `provider:"local-docker"`.
|
|
11
|
-
*
|
|
12
|
-
* 🔴 **Isolation, NOT a snapshot sandbox** (DUAL-MODE-DESIGN §5 / design/61 §9):
|
|
13
|
-
* - `capabilities = { isolation: true, suspendable: false }`. The container is a true isolation boundary
|
|
14
|
-
* (separate filesystem/PID/mount namespaces), so the agent's writes/exec are confined to it and parallel
|
|
15
|
-
* fan-out tasks don't collide. But v1 is **NOT suspendable** — `suspendVM`/`resumeVM`/`postResumeInit`
|
|
16
|
-
* return typed `"unsupported"`. (`docker pause`/`docker commit` could give a coarse suspend later; we do
|
|
17
|
-
* NOT fake it here — durable suspend is honestly N/A until that lands.)
|
|
18
|
-
* - `destroy()` / `cleanup()` **`docker rm -f` the container** — a full teardown (the container IS the
|
|
19
|
-
* per-task sandbox, unlike host which only `rm -rf`s a dir). Idempotent, best-effort, never throws.
|
|
20
|
-
* - `reconnect()` is supported as a liveness re-check: if the container is still running it is an idempotent
|
|
21
|
-
* success; if it's gone (crashed / externally `rm`'d) it FAILS (`connect_failed`) — there is no managed
|
|
22
|
-
* snapshot to resume from, so a dead container cannot be re-attached.
|
|
23
|
-
*
|
|
24
|
-
* 🔴 **Domestic-images iron rule (clay):** the base `image` comes ENTIRELY from config (no hardcoded
|
|
25
|
-
* `docker.io/...` default). A deployment points it at the sandbox-image-pool (docker.io/claybobby public pool)
|
|
26
|
-
* or its own private registry — SWR was deprecated 2026-07-13 and the in-network Gitea registry container was
|
|
27
|
-
* retired 2026-08 (no CN fallback is baked in). This adapter never invents a registry.
|
|
28
|
-
*
|
|
29
|
-
* 🔴 **argv anti-injection (council, host/ssh §4):** every `docker` invocation is a `spawn(binary, argv[])`
|
|
30
|
-
* with NO host-side shell (`shell:false` default) — the container id, image, paths, and env values are
|
|
31
|
-
* SEPARATE argv elements, never string-concatenated into a `sh -c` on the host. The agent's COMMAND itself is
|
|
32
|
-
* still a shell command (it runs the container's `/bin/sh -c <command>`, exactly like the host/ssh adapters
|
|
33
|
-
* run the operator's command in a shell) — that is the agent's intended surface, bounded by the container.
|
|
34
|
-
*
|
|
35
|
-
* **never-throw invariant:** every base `ExecutionEnv` FileSystem/Shell method encodes failure in its returned
|
|
36
|
-
* `Result` (never throws) — docker missing from PATH, a command timeout, a container that won't start, all
|
|
37
|
-
* become `Result.error`. `execStream`'s iterator MAY throw `RemoteExecutionError` mid-stream before an `exit`
|
|
38
|
-
* chunk (the seam contract). A non-zero command exit is a NORMAL value (`{ ok:true, exitCode }`), never an error.
|
|
39
|
-
*/
|
|
40
1
|
import path from "node:path";
|
|
41
2
|
import os from "node:os";
|
|
42
3
|
import fs from "node:fs/promises";
|
|
@@ -48,13 +9,8 @@ import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, mar
|
|
|
48
9
|
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
49
10
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
50
11
|
const PROVIDER = "local-docker";
|
|
51
|
-
/** Default workspace root INSIDE the container (the agent's tools resolve relative paths against it). */
|
|
52
12
|
const DEFAULT_MOUNT_PATH = "/workspace";
|
|
53
|
-
/** Per-command wall-clock when the caller passes none (a single exec bound, NOT the container lifetime).
|
|
54
|
-
* ⚠️ 同判据三腿之一(另两条:`remote-env-host.ts` 同值 30min、`remote-env-ssh.ts` **5min**)——分歧成文
|
|
55
|
-
* 与候裁说明写在 ssh 那处的 `commandTimeoutMs` 顶注(#176 二轮扫描 finding)。 */
|
|
56
13
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
57
|
-
/** Wall-clock for a control-plane docker op (run/rm/inspect/cp) — should be fast; a hang surfaces typed. */
|
|
58
14
|
const DEFAULT_CONTROL_TIMEOUT_MS = 60_000;
|
|
59
15
|
const ok = (value) => ({ ok: true, value });
|
|
60
16
|
const unsupported = (op) => ({
|
|
@@ -62,27 +18,18 @@ const unsupported = (op) => ({
|
|
|
62
18
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a local-docker target (v1: a container is an isolation boundary but not a managed memory snapshot; capabilities.suspendable=false)`),
|
|
63
19
|
});
|
|
64
20
|
export class RemoteLocalDockerExecutionEnv {
|
|
65
|
-
/** DUAL-MODE-DESIGN §5: a container is a real isolation boundary; v1 has no managed memory snapshot. */
|
|
66
21
|
capabilities = { isolation: true, suspendable: false };
|
|
67
|
-
/** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
|
|
68
22
|
cwd;
|
|
69
23
|
cfg;
|
|
70
24
|
spawnFn;
|
|
71
|
-
/** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
|
|
72
25
|
containerName;
|
|
73
|
-
/** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */
|
|
74
26
|
providerName;
|
|
75
|
-
/** Set once the container is up + the workspace dir created. */
|
|
76
27
|
containerId;
|
|
77
28
|
handle;
|
|
78
|
-
/** Terminal once {@link destroy} ran — guards exec/fs from running against a removed container. */
|
|
79
29
|
destroyed = false;
|
|
80
|
-
/** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */
|
|
81
30
|
connecting;
|
|
82
31
|
constructor(config, deps = {}) {
|
|
83
32
|
if (!config.image || !config.image.trim()) {
|
|
84
|
-
// image is required (no docker.io default) — but the constructor must not throw (never-throw posture):
|
|
85
|
-
// record the empty image and let connect() fail closed with a typed error.
|
|
86
33
|
config = { ...config, image: config.image ?? "" };
|
|
87
34
|
}
|
|
88
35
|
this.cfg = {
|
|
@@ -96,7 +43,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
96
43
|
this.cwd = this.cfg.mountPath;
|
|
97
44
|
this.spawnFn = deps.spawn ?? spawn;
|
|
98
45
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
99
|
-
const suffix = randomBytes(4).toString("hex");
|
|
46
|
+
const suffix = randomBytes(4).toString("hex");
|
|
100
47
|
this.containerName = `sema-docker-${sanitizeId(id)}-${suffix}`;
|
|
101
48
|
this.providerName = config.providerLabel?.trim() || PROVIDER;
|
|
102
49
|
}
|
|
@@ -108,31 +55,27 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
108
55
|
throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
|
|
109
56
|
return this.handle;
|
|
110
57
|
}
|
|
111
|
-
// ─────────────────────────────── lifecycle ───────────────────────────────
|
|
112
58
|
async connect(config) {
|
|
113
59
|
if (config?.snapshotId)
|
|
114
|
-
return unsupported("connect(snapshotId)");
|
|
60
|
+
return unsupported("connect(snapshotId)");
|
|
115
61
|
if (this.destroyed)
|
|
116
62
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
117
63
|
if (config?.abortSignal?.aborted)
|
|
118
64
|
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
119
65
|
if (this.containerId)
|
|
120
|
-
return ok(this.workspaceHandle());
|
|
66
|
+
return ok(this.workspaceHandle());
|
|
121
67
|
if (this.connecting)
|
|
122
|
-
return this.connecting;
|
|
68
|
+
return this.connecting;
|
|
123
69
|
this.connecting = this.doConnect(config);
|
|
124
70
|
const r = await this.connecting;
|
|
125
71
|
if (!r.ok)
|
|
126
|
-
this.connecting = undefined;
|
|
72
|
+
this.connecting = undefined;
|
|
127
73
|
return r;
|
|
128
74
|
}
|
|
129
75
|
async doConnect(config) {
|
|
130
76
|
if (!this.cfg.image.trim()) {
|
|
131
77
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "local-docker: no base image configured (the image is required — no docker.io default)") };
|
|
132
78
|
}
|
|
133
|
-
// Start a long-lived detached container that idles forever (the agent's commands are `docker exec`'d into
|
|
134
|
-
// it). `tail -f /dev/null` is the most portable idle keep-alive (no `sleep infinity` dependency on some
|
|
135
|
-
// busybox/alpine images). --init reaps zombie exec children. The workspace dir is created via the same run.
|
|
136
79
|
const pidsLimit = this.cfg.pidsLimit ?? 512;
|
|
137
80
|
const runArgs = [
|
|
138
81
|
"run",
|
|
@@ -142,8 +85,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
142
85
|
this.containerName,
|
|
143
86
|
"-w",
|
|
144
87
|
this.cfg.mountPath,
|
|
145
|
-
// Always-safe sandbox hardening (no build breakage): block setuid privilege escalation + cap the process
|
|
146
|
-
// count so generated code can't fork-bomb the shared host kernel. cap-drop is opt-in (it DOES break some builds).
|
|
147
88
|
"--security-opt",
|
|
148
89
|
"no-new-privileges",
|
|
149
90
|
...(pidsLimit > 0 ? ["--pids-limit", String(pidsLimit)] : []),
|
|
@@ -154,19 +95,17 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
154
95
|
this.cfg.image,
|
|
155
96
|
"/bin/sh",
|
|
156
97
|
"-c",
|
|
157
|
-
// Create the workspace then idle. mkdir -p is harmless if the image already has it.
|
|
158
98
|
`mkdir -p ${shellQuote(this.cfg.mountPath)} && exec tail -f /dev/null`,
|
|
159
99
|
];
|
|
160
100
|
const run = await this.docker(runArgs, { signal: config?.abortSignal, timeoutMs: this.cfg.controlTimeoutMs });
|
|
161
101
|
if (!run.ok) {
|
|
162
|
-
await this.forceRemove().catch(() => { });
|
|
102
|
+
await this.forceRemove().catch(() => { });
|
|
163
103
|
return { ok: false, error: this.dockerToRemoteError(run.error, "connect_failed") };
|
|
164
104
|
}
|
|
165
105
|
if (run.value.exitCode !== 0) {
|
|
166
106
|
await this.forceRemove().catch(() => { });
|
|
167
107
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `docker run failed: ${run.value.stderr.trim() || `exit ${run.value.exitCode}`}`) };
|
|
168
108
|
}
|
|
169
|
-
// Race with destroy() (the awaits above yield): a destroy that landed must win — remove + bail (no orphan).
|
|
170
109
|
if (this.destroyed) {
|
|
171
110
|
await this.forceRemove().catch(() => { });
|
|
172
111
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
@@ -174,7 +113,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
174
113
|
this.containerId = run.value.stdout.trim() || this.containerName;
|
|
175
114
|
this.handle = { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
176
115
|
if (config?.secrets?.length) {
|
|
177
|
-
// Secrets are injected per-command via cfg.env (resolved by the control plane), not at connect — record only.
|
|
178
116
|
}
|
|
179
117
|
return ok(this.handle);
|
|
180
118
|
}
|
|
@@ -187,11 +125,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
187
125
|
async postResumeInit() {
|
|
188
126
|
return unsupported("postResumeInit");
|
|
189
127
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Liveness re-check (NOT a snapshot resume): if the container is still running → idempotent success; if it's
|
|
192
|
-
* gone (crashed / externally removed) → `connect_failed` (no managed snapshot to re-attach to). `sessionToken`
|
|
193
|
-
* is the container name (== our handle id); a mismatch with this env's container fails closed.
|
|
194
|
-
*/
|
|
195
128
|
async reconnect(sessionToken) {
|
|
196
129
|
if (this.destroyed)
|
|
197
130
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
@@ -200,39 +133,28 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
200
133
|
}
|
|
201
134
|
const insp = await this.docker(["inspect", "-f", "{{.State.Running}}", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs });
|
|
202
135
|
if (insp.ok && insp.value.exitCode === 0 && insp.value.stdout.trim() === "true") {
|
|
203
|
-
// 🔴 mark CONNECTED to the live container: else ensureConnectedExec/fsReady (gate on containerId) would
|
|
204
|
-
// re-run `docker run --name <same>` → name conflict → the failure path rm -f's the very container we just
|
|
205
|
-
// re-attached to (destroying the running workspace). The name IS our exec/handle target.
|
|
206
136
|
this.containerId = this.containerName;
|
|
207
137
|
this.handle ??= { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
208
138
|
return ok(this.handle);
|
|
209
139
|
}
|
|
210
140
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `container '${this.containerName}' is not running (gone or stopped) — no managed snapshot to resume`) };
|
|
211
141
|
}
|
|
212
|
-
/** `docker rm -f` the container (full teardown). Idempotent, best-effort, never throws (cleanup contract). */
|
|
213
142
|
async destroy() {
|
|
214
143
|
if (this.destroyed)
|
|
215
|
-
return;
|
|
144
|
+
return;
|
|
216
145
|
this.destroyed = true;
|
|
217
|
-
// 🔴 AWAIT any racing `docker run` before reaping: a destroy that lands mid-connect must not resolve before
|
|
218
|
-
// the container exists, else forceRemove (by name) no-ops now and the just-created container LEAKS forever
|
|
219
|
-
// (docker has no self-timeout). Mirrors the host adapter's await-prepare-then-rm.
|
|
220
146
|
const inflight = this.connecting;
|
|
221
147
|
this.connecting = undefined;
|
|
222
148
|
this.containerId = undefined;
|
|
223
149
|
await inflight?.catch(() => { });
|
|
224
|
-
// Remove by NAME (covers a container that started but whose id we never recorded) — `rm -f` no-ops a
|
|
225
|
-
// never-created name; best-effort, never throws.
|
|
226
150
|
await this.forceRemove().catch(() => { });
|
|
227
151
|
}
|
|
228
152
|
async cleanup() {
|
|
229
153
|
return this.destroy();
|
|
230
154
|
}
|
|
231
155
|
async forceRemove() {
|
|
232
|
-
// best-effort, short bound; swallow everything (teardown must never throw).
|
|
233
156
|
await this.docker(["rm", "-f", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs }).catch(() => { });
|
|
234
157
|
}
|
|
235
|
-
// ─────────────────────────────── Shell: buffered exec ───────────────────────────────
|
|
236
158
|
async exec(command, options) {
|
|
237
159
|
if (options?.abortSignal?.aborted)
|
|
238
160
|
return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
|
|
@@ -248,10 +170,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
248
170
|
});
|
|
249
171
|
if (!r.ok)
|
|
250
172
|
return { ok: false, error: r.error };
|
|
251
|
-
// A non-zero exit is a NORMAL value (ExecutionEnv contract) — `docker exec` propagates the inner exit code.
|
|
252
173
|
return ok({ stdout: r.value.stdout, stderr: r.value.stderr, exitCode: r.value.exitCode });
|
|
253
174
|
}
|
|
254
|
-
// ─────────────────────────────── streaming exec ───────────────────────────────
|
|
255
175
|
async *execStream(command, options) {
|
|
256
176
|
const ready = await this.ensureConnectedExec();
|
|
257
177
|
if (!ready.ok)
|
|
@@ -279,7 +199,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
279
199
|
if (queue.length >= 512 && !paused) {
|
|
280
200
|
paused = true;
|
|
281
201
|
child.stdout?.pause();
|
|
282
|
-
}
|
|
202
|
+
}
|
|
283
203
|
signalReady();
|
|
284
204
|
};
|
|
285
205
|
child.stdout?.on("data", (d) => enqueue("stdout", d.toString()));
|
|
@@ -307,7 +227,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
307
227
|
try {
|
|
308
228
|
child.kill("SIGKILL");
|
|
309
229
|
}
|
|
310
|
-
catch {
|
|
230
|
+
catch { }
|
|
311
231
|
}
|
|
312
232
|
};
|
|
313
233
|
if (options?.signal) {
|
|
@@ -319,7 +239,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
319
239
|
try {
|
|
320
240
|
child.kill("SIGKILL");
|
|
321
241
|
}
|
|
322
|
-
catch {
|
|
242
|
+
catch { }
|
|
323
243
|
signalReady();
|
|
324
244
|
};
|
|
325
245
|
if (options.signal.aborted)
|
|
@@ -337,7 +257,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
337
257
|
try {
|
|
338
258
|
child.kill("SIGKILL");
|
|
339
259
|
}
|
|
340
|
-
catch {
|
|
260
|
+
catch { }
|
|
341
261
|
throw new RemoteExecutionError("unknown", `execStream output exceeded maxOutputBytes=${cap}`);
|
|
342
262
|
}
|
|
343
263
|
}
|
|
@@ -350,7 +270,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
350
270
|
if (finished)
|
|
351
271
|
break;
|
|
352
272
|
const waitMs = idleMs != null ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
|
|
353
|
-
// [4947] A-002.11 sibling: clear the losing timer — wake() firing first used to leave it armed.
|
|
354
273
|
let waitTimer;
|
|
355
274
|
try {
|
|
356
275
|
await new Promise((r) => { wake = r; waitTimer = setTimeout(r, waitMs); });
|
|
@@ -364,7 +283,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
364
283
|
try {
|
|
365
284
|
child.kill("SIGKILL");
|
|
366
285
|
}
|
|
367
|
-
catch {
|
|
286
|
+
catch { }
|
|
368
287
|
throw new RemoteExecutionError("timeout", `execStream idle > ${idleMs}ms (command hang)`);
|
|
369
288
|
}
|
|
370
289
|
}
|
|
@@ -378,7 +297,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
378
297
|
teardown();
|
|
379
298
|
}
|
|
380
299
|
}
|
|
381
|
-
// ─────────────────────────────── FileSystem ───────────────────────────────
|
|
382
300
|
async absolutePath(p) {
|
|
383
301
|
return ok(this.resolve(p));
|
|
384
302
|
}
|
|
@@ -390,13 +308,12 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
390
308
|
if (!ready.ok)
|
|
391
309
|
return ready;
|
|
392
310
|
const abs = this.resolve(p);
|
|
393
|
-
// `docker exec ... cat <file>` streams raw bytes on stdout (binary-safe); stderr carries cat's error.
|
|
394
311
|
const r = await this.docker(this.execRaw(`cat ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, binary: true });
|
|
395
312
|
if (!r.ok)
|
|
396
313
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
397
314
|
if (r.value.exitCode !== 0)
|
|
398
315
|
return { ok: false, error: classifyFsStderr(r.value.stderr, abs) };
|
|
399
|
-
return ok(r.value.stdoutBytes ?? new Uint8Array());
|
|
316
|
+
return ok(r.value.stdoutBytes ?? new Uint8Array());
|
|
400
317
|
}
|
|
401
318
|
async readTextFile(p, abortSignal) {
|
|
402
319
|
const r = await this.readBinaryFile(p, abortSignal);
|
|
@@ -418,7 +335,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
418
335
|
if (!ready.ok)
|
|
419
336
|
return ready;
|
|
420
337
|
const abs = this.resolve(p);
|
|
421
|
-
// Create parent dirs inside the container (parity with the host/e2b adapters' auto-create).
|
|
422
338
|
const dir = path.posix.dirname(abs);
|
|
423
339
|
if (dir && dir !== "." && dir !== "/") {
|
|
424
340
|
const mk = await this.docker(this.execRaw(`mkdir -p ${shellQuote(dir)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
@@ -427,8 +343,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
427
343
|
if (mk.value.exitCode !== 0)
|
|
428
344
|
return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
|
|
429
345
|
}
|
|
430
|
-
// Stage to a host temp file then `docker cp` (binary-safe; avoids shell-escaping the payload — same
|
|
431
|
-
// discipline as the ADB adapter's `adb push`). `docker cp <src> <name>:<abs>` writes into the container.
|
|
432
346
|
let local;
|
|
433
347
|
try {
|
|
434
348
|
local = path.join(os.tmpdir(), `docker-cp-${Date.now()}-${randomBytes(6).toString("hex")}`);
|
|
@@ -464,14 +378,9 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
464
378
|
merged.set(add, base.length);
|
|
465
379
|
return this.writeFile(abs, merged, abortSignal);
|
|
466
380
|
}
|
|
467
|
-
// ── design/158 B6:POSIX shell-out 文件面全部委托共享基座(posix-shell-fs.ts)─────────────────────
|
|
468
|
-
// 六裁决与 readLink・lstat 契约见基座头注。本腿此前的 fsErrorFromStderr 无 not_found 缺省(缺文件报
|
|
469
|
-
// unknown),且 `|not found` 宽包含会把「命令缺失」误判成「文件不存在」—— 两处都被基座正形纠正。
|
|
470
381
|
posixFsInst;
|
|
471
382
|
get posixFs() {
|
|
472
383
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
473
|
-
// 承 fsReady 契约(其余三腿走公有 exec、守卫天然在;本腿直用私有 docker,必须自带):
|
|
474
|
-
// destroyed 守卫 + 懒连接,fail closed 不抛 —— 消息保留 "destroyed" 字样(测试钉的就是它)。
|
|
475
384
|
run: async (cmd, abortSignal) => {
|
|
476
385
|
if (this.destroyed)
|
|
477
386
|
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
@@ -513,63 +422,43 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
513
422
|
async createTempFile(options) {
|
|
514
423
|
return this.posixFs.createTempFile(options);
|
|
515
424
|
}
|
|
516
|
-
// ─────────────────────────────── internals ───────────────────────────────
|
|
517
|
-
/** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
|
|
518
425
|
withHostFlag(args) {
|
|
519
426
|
return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
|
|
520
427
|
}
|
|
521
|
-
/** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
|
|
522
|
-
* argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`. */
|
|
523
428
|
execArgv(command, cwd, env) {
|
|
524
429
|
const args = ["exec"];
|
|
525
430
|
const dir = cwd != null ? this.resolve(cwd) : this.cwd;
|
|
526
431
|
args.push("-w", dir);
|
|
527
|
-
const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
|
|
432
|
+
const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
|
|
528
433
|
for (const [k, v] of Object.entries(mergedEnv))
|
|
529
|
-
args.push("-e", `${k}=${v}`);
|
|
530
|
-
args.push(this.containerName, this.containerShell, "-c", command);
|
|
434
|
+
args.push("-e", `${k}=${v}`);
|
|
435
|
+
args.push(this.containerName, this.containerShell, "-c", command);
|
|
531
436
|
return args;
|
|
532
437
|
}
|
|
533
|
-
/** Like {@link execArgv} but for an internal FileSystem helper command (uses the workspace cwd + adapter env). */
|
|
534
438
|
execRaw(command) {
|
|
535
439
|
return this.execArgv(command);
|
|
536
440
|
}
|
|
537
|
-
/** Spawn the docker binary with argv; collect stdout/stderr (+ raw bytes when binary), bound by timeout/abort. */
|
|
538
441
|
docker(args, opts) {
|
|
539
442
|
return new Promise((resolve) => {
|
|
540
443
|
let settled = false;
|
|
541
|
-
// D5 pipe-destroy grace: armed by killGrace() on abort/timeout; destroys stdout/stderr pipes if `close`
|
|
542
|
-
// never fires (a re-parented grandchild holding the FD). Cleared in finish() — the real `close` routes
|
|
543
|
-
// through finish() — so the timer never leaks. Does NOT change the synchronous settle below.
|
|
544
444
|
let forceSettleTimer;
|
|
545
445
|
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"] });
|
|
546
|
-
// SIGKILL the child then arm the D5 pipe-destroy grace (only on the abort/timeout paths — a normal `close`
|
|
547
|
-
// never calls this). Idempotent: only one timer is ever armed.
|
|
548
446
|
const killGrace = () => {
|
|
549
447
|
try {
|
|
550
448
|
child.kill("SIGKILL");
|
|
551
449
|
}
|
|
552
|
-
catch {
|
|
450
|
+
catch { }
|
|
553
451
|
forceSettleTimer ??= armPipeDestroyGrace(child);
|
|
554
452
|
};
|
|
555
|
-
// 🔴 binary caveat (D2): this helper is SHARED by the model-facing TEXT exec (must be bounded — a
|
|
556
|
-
// `yes`/`cat big` could OOM the durable worker) and by readBinaryFile (`docker exec ... cat`, opts.binary=true,
|
|
557
|
-
// which reads stdoutBytes and MUST keep EVERY byte or binary/large reads silently corrupt). So we branch:
|
|
558
|
-
// - binary=true → full Buffer[] + concat (byte-exact stdoutBytes; readBinaryFile path).
|
|
559
|
-
// - binary=false → bounded rolling-TAIL for stdout (keep last 8MB + dropped-byte marker, O(maxBytes));
|
|
560
|
-
// no text caller reads stdoutBytes, so it is derived from the bounded text (DockerRun.stdoutBytes is non-optional).
|
|
561
|
-
// stderr is ALWAYS bounded — it never carries the binary payload (just cat's error text).
|
|
562
453
|
const binary = !!opts?.binary;
|
|
563
|
-
const outBufs = [];
|
|
564
|
-
const outTail = binary ? undefined : new RollingTailBuffer();
|
|
454
|
+
const outBufs = [];
|
|
455
|
+
const outTail = binary ? undefined : new RollingTailBuffer();
|
|
565
456
|
const errBuf = new RollingTailBuffer();
|
|
566
457
|
const finish = (r) => {
|
|
567
458
|
if (settled)
|
|
568
459
|
return;
|
|
569
460
|
settled = true;
|
|
570
461
|
clearTimeout(timer);
|
|
571
|
-
// D5 grace cleared in the `close` handler (pipes closed naturally), NOT here — abort/timeout reaches
|
|
572
|
-
// finish() BEFORE `close`, so clearing here would cancel the grace before it can free a leaked FD.
|
|
573
462
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
574
463
|
resolve(r);
|
|
575
464
|
};
|
|
@@ -590,33 +479,26 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
590
479
|
child.stdout?.on("data", (d) => { if (binary)
|
|
591
480
|
outBufs.push(d);
|
|
592
481
|
else
|
|
593
|
-
outTail.push(d); opts?.onStdout?.(d.toString()); });
|
|
482
|
+
outTail.push(d); opts?.onStdout?.(d.toString()); });
|
|
594
483
|
child.stderr?.on("data", (d) => { errBuf.push(d); opts?.onStderr?.(d.toString()); });
|
|
595
484
|
child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `docker spawn failed (is '${this.cfg.dockerPath}' installed?): ${e.message}`, e) }));
|
|
596
485
|
child.on("close", (code, signal) => {
|
|
597
486
|
if (forceSettleTimer)
|
|
598
|
-
clearTimeout(forceSettleTimer);
|
|
599
|
-
// A signal-killed process (external SIGKILL/OOM) has code===null → derive the conventional 128+signo
|
|
600
|
-
// (parity with execStream + the host adapter); NEVER report a killed command as a success exitCode 0.
|
|
487
|
+
clearTimeout(forceSettleTimer);
|
|
601
488
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 1);
|
|
602
489
|
const err = errBuf.result();
|
|
603
490
|
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
604
491
|
if (binary) {
|
|
605
|
-
const outBytes = Buffer.concat(outBufs);
|
|
492
|
+
const outBytes = Buffer.concat(outBufs);
|
|
606
493
|
finish(ok({ stdout: outBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(outBytes) }));
|
|
607
494
|
}
|
|
608
495
|
else {
|
|
609
|
-
const out = outTail.result();
|
|
496
|
+
const out = outTail.result();
|
|
610
497
|
finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode }));
|
|
611
498
|
}
|
|
612
499
|
});
|
|
613
500
|
});
|
|
614
501
|
}
|
|
615
|
-
/** Lazy connect for an exec path → ExecutionError on failure (fail closed, never throws). */
|
|
616
|
-
/** S1([1870]):容器内**用户命令**的 shell。缺省 /bin/sh;首次连接后探测一次 `command -v bash`,
|
|
617
|
-
* 有则切 bash(TB 实测 dash 三病:$'\t' 静默错字节/[[ ]] 127/进程替换)。镜像无 bash(alpine 裸底)
|
|
618
|
-
* ⇒ 保 sh(修前字节形,诚实降级);探测自身失败同。容器**创建**引导命令(mkdir && tail)不走
|
|
619
|
-
* 本 shell——无 bash 语法且探测尚不可能。 */
|
|
620
502
|
containerShell = "/bin/sh";
|
|
621
503
|
containerShellProbed = false;
|
|
622
504
|
async probeContainerShell() {
|
|
@@ -624,23 +506,18 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
624
506
|
return;
|
|
625
507
|
this.containerShellProbed = true;
|
|
626
508
|
try {
|
|
627
|
-
// 🔴 NO withHostFlag here — `docker()` already prepends it. A second `-H` is not a harmless repeat: the
|
|
628
|
-
// docker CLI rejects the whole command (`invalid argument … specify only one -H`), so on every
|
|
629
|
-
// REMOTE-daemon container (`dockerHost` set — always, on the `remote-docker` arm) the probe used to fail
|
|
630
|
-
// and silently degrade the user shell to /bin/sh for a reason unrelated to the image.
|
|
631
509
|
const r = await this.docker(["exec", this.containerName, "/bin/sh", "-c", "command -v bash"], { timeoutMs: this.cfg.controlTimeoutMs });
|
|
632
510
|
if (r.ok && r.value.exitCode === 0 && r.value.stdout.trim().length > 0)
|
|
633
511
|
this.containerShell = "bash";
|
|
634
512
|
}
|
|
635
513
|
catch {
|
|
636
|
-
/* 保 sh —— 探测绝不 fault 连接路径 */
|
|
637
514
|
}
|
|
638
515
|
}
|
|
639
516
|
async ensureConnectedExec() {
|
|
640
517
|
if (this.destroyed)
|
|
641
518
|
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
642
519
|
if (this.containerId) {
|
|
643
|
-
await this.probeContainerShell();
|
|
520
|
+
await this.probeContainerShell();
|
|
644
521
|
return ok(undefined);
|
|
645
522
|
}
|
|
646
523
|
const c = await this.connect();
|
|
@@ -649,7 +526,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
649
526
|
await this.probeContainerShell();
|
|
650
527
|
return ok(undefined);
|
|
651
528
|
}
|
|
652
|
-
/** Lazy connect for a FileSystem path → FileError on failure (fail closed, never throws). */
|
|
653
529
|
async fsReady(p) {
|
|
654
530
|
if (this.destroyed)
|
|
655
531
|
return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
|
|
@@ -660,7 +536,6 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
660
536
|
return { ok: false, error: new FileError("unknown", `local-docker connect failed: ${c.error.message}`, this.resolve(p), c.error) };
|
|
661
537
|
return ok(undefined);
|
|
662
538
|
}
|
|
663
|
-
/** Map an ExecutionError from a docker CONTROL op (run/inspect) onto a RemoteExecutionError code. */
|
|
664
539
|
dockerToRemoteError(e, fallback) {
|
|
665
540
|
if (e.code === "timeout")
|
|
666
541
|
return new RemoteExecutionError("timeout", e.message, e);
|
|
@@ -669,20 +544,12 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
669
544
|
return new RemoteExecutionError(fallback, e.message, e);
|
|
670
545
|
}
|
|
671
546
|
}
|
|
672
|
-
/** Strip anything but [A-Za-z0-9._-] from an id so it is safe in a docker container name. */
|
|
673
547
|
function sanitizeId(id) {
|
|
674
548
|
return id.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 48) || "task";
|
|
675
549
|
}
|
|
676
550
|
function errMsg(e) {
|
|
677
551
|
return e instanceof Error ? e.message : String(e);
|
|
678
552
|
}
|
|
679
|
-
/**
|
|
680
|
-
* `ExecutionEnvFactory` for the TOC `local-docker` backend (DUAL-MODE-DESIGN §5). Wiring this onto a deployment
|
|
681
|
-
* makes its agent run each task in a per-task container on the worker's OWN docker daemon (isolation:true,
|
|
682
|
-
* suspendable:false). Returns the env UNCONNECTED (lazy — the container starts on first fs/exec use); the Runner
|
|
683
|
-
* owns the lifetime and calls `destroy()` (= `docker rm -f`) on task end. Folds the per-task `ctx.taskId`/
|
|
684
|
-
* `ctx.sessionId` into the container name so two concurrent tasks never share a container.
|
|
685
|
-
*/
|
|
686
553
|
export function localDockerExecutionEnvFactory(config, deps = {}) {
|
|
687
554
|
return (ctx) => new RemoteLocalDockerExecutionEnv({ ...config, id: config.id ?? ctx.taskId ?? ctx.sessionId }, deps);
|
|
688
555
|
}
|