@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,24 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `RemoteSshExecutionEnv` — the SSH implementation of core's `RemoteExecutionEnv` seam (design/61). A PEER
|
|
3
|
-
* adapter to the E2B one (NOT chained): `docker(agent) → [SSH adapter] → real machine`. Use case: an AI
|
|
4
|
-
* orchestrating batch deployment over SSH-reachable hosts.
|
|
5
|
-
*
|
|
6
|
-
* 🔴 **SSH targets a REAL system, not a disposable sandbox** (design/61 §5):
|
|
7
|
-
* - `capabilities = { isolation: false, suspendable: false }` — actions are PERMANENT, no snapshot. The
|
|
8
|
-
* orchestrator/leader tightens autonomy + the design/37 policy gate + HITL on a non-isolated env (design/53).
|
|
9
|
-
* - `destroy()` / `cleanup()` **close the SSH connection** — they do NOT destroy the machine (vs E2B's
|
|
10
|
-
* teardown). Anything the agent created on the host PERSISTS (cleanup is the gate/operator's job).
|
|
11
|
-
* - `suspendVM`/`resumeVM`/`postResumeInit` → typed `"unsupported"` (a real machine isn't snapshotable;
|
|
12
|
-
* design/45 durable suspend N/A). `reconnect` re-establishes the connection.
|
|
13
|
-
*
|
|
14
|
-
* 🔴 **Credential isolation (design/61 §5 #2 / design/53):** the private key is held by the control plane and
|
|
15
|
-
* injected to the ADAPTER as a secret env (`SecretRef.destination:"adapter"`) — it NEVER enters the workspace
|
|
16
|
-
* / agent-reachable surface. The agent operates via exec/sftp; it cannot read the key.
|
|
17
|
-
*
|
|
18
|
-
* Liveness: every SSH op is a network round-trip that can hang — `connect` is wrapped in `withRetry` (idempotent
|
|
19
|
-
* establishment only), exec/sftp carry a bounded timeout, and a hang surfaces a typed retryable error (the
|
|
20
|
-
* shared timeout/liveness contract, reused).
|
|
21
|
-
*/
|
|
22
1
|
import path from "node:path";
|
|
23
2
|
import { Client } from "ssh2";
|
|
24
3
|
import { shellQuote, kindFromMode } from "./remote-shell.js";
|
|
@@ -32,30 +11,16 @@ const unsupported = (op) => ({
|
|
|
32
11
|
ok: false,
|
|
33
12
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on an SSH target (real machine — not snapshotable; capabilities.suspendable=false)`),
|
|
34
13
|
});
|
|
35
|
-
/** ssh2's ClientChannel "close" event gives the killing signal's BARE POSIX name (e.g. "KILL"), unlike Node's
|
|
36
|
-
* own `child.on("close")` which gives `NodeJS.Signals` names (e.g. "SIGKILL") — re-prefix before looking it
|
|
37
|
-
* up in the shared, authoritative signal table (host-platform.ts `signalNumber`; single pattern-home, no
|
|
38
|
-
* hand-written table here). `?? 9` matches the convention every other exec adapter uses for an unmapped name. */
|
|
39
14
|
function exitCodeForSignalKill(signal) {
|
|
40
15
|
return 128 + (signalNumber(`SIG${signal}`) ?? 9);
|
|
41
16
|
}
|
|
42
17
|
export class RemoteSshExecutionEnv {
|
|
43
18
|
capabilities = { isolation: false, suspendable: false };
|
|
44
|
-
/** Working directory; relative paths resolve against it (ExecutionEnv contract). Starts at mountPath. */
|
|
45
19
|
cwd;
|
|
46
20
|
conn;
|
|
47
21
|
sftpPromise;
|
|
48
22
|
destroyed = false;
|
|
49
|
-
/** [1165] 代际闸:destroy/reconnect 各自 +1。逻辑连接操作(startConnect)起飞时抓拍一次并贯穿全部
|
|
50
|
-
* 重试轮(四轮复审:抓拍若在 connectOnce 内,被顶替代的下一轮重试会抓到**新**代际混进新代)——
|
|
51
|
-
* 每轮起跳先查代际/destroyed(陈旧=不再开 socket),ready 到达再查一次(在飞握手 late-ready=end
|
|
52
|
-
* 不赋 conn)。关死「A 在飞→reconnect 起 B→A 竞后抢占/成孤儿活连接」的全部竞窗。 */
|
|
53
23
|
connEpoch = 0;
|
|
54
|
-
/** [1165] 四轮复审 single-flight:connect()/reconnect()/懒连接三路共用一条代际键在飞记录——任何一路
|
|
55
|
-
* 在飞时其余路 join 同一 promise(同代际),绝不并起两条连接;settle 时按记录身份清(旧败诺清不掉
|
|
56
|
-
* 新记录)。五轮复审:①记录带 kind——并发 reconnect join 已在飞的 replace 记录(不再各自 bump 代际
|
|
57
|
-
* =重连风暴);②共享尝试**不携带任何 caller 信号**(首创者的 pre-abort 不得毒化 joiner;单 caller
|
|
58
|
-
* 取消只退它自己的等待,底层握手余命 ≤ readyTimeout 有界,彻底取消归 destroy 代际闸)。 */
|
|
59
24
|
inFlight;
|
|
60
25
|
cfg;
|
|
61
26
|
injectedConnectOnce;
|
|
@@ -81,20 +46,13 @@ export class RemoteSshExecutionEnv {
|
|
|
81
46
|
workspaceHandle() {
|
|
82
47
|
return { sandboxId: `${this.cfg.host}:${this.cfg.port}`, provider: PROVIDER, mountPath: this.cwd };
|
|
83
48
|
}
|
|
84
|
-
// ─────────────────────────────── lifecycle ───────────────────────────────
|
|
85
49
|
async connect(config) {
|
|
86
|
-
// [1165] 三轮复审:destroy 后一律不复活(显式 connect/reconnect 同禁)——destroyed env 是 teardown
|
|
87
|
-
// 终态,复活=任务清理后还能执行命令/留活连接(资源与安全双漏)。恢复流程走工厂新铸实例。
|
|
88
50
|
if (this.destroyed)
|
|
89
51
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
|
|
90
|
-
// 已连=幂等成功(四轮复审:旧形二次 connect 会并起第二条连接顶掉第一条=孤儿活连接)。
|
|
91
52
|
if (this.conn)
|
|
92
53
|
return ok(this.workspaceHandle());
|
|
93
|
-
// 五轮复审:caller 信号只管 caller 自己的等待(race),共享尝试不沾——pre-abort 的首创者不得
|
|
94
|
-
// 毒化同拍 joiner,反向 pre-abort 的 joiner 也必须即时退出。
|
|
95
54
|
return this.raceConnAbort(this.startConnect("establish"), config?.abortSignal);
|
|
96
55
|
}
|
|
97
|
-
/** 五轮复审:per-caller 可中止等待(共享诺不动,只退本 caller)。 */
|
|
98
56
|
async raceConnAbort(shared, signal) {
|
|
99
57
|
if (!signal)
|
|
100
58
|
return shared;
|
|
@@ -112,15 +70,12 @@ export class RemoteSshExecutionEnv {
|
|
|
112
70
|
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while waiting") };
|
|
113
71
|
return raced;
|
|
114
72
|
}
|
|
115
|
-
/** [1165] 四轮复审:三路(connect/reconnect/懒连接)唯一的连接发起点——同代际在飞记录 join,settle
|
|
116
|
-
* 身份清;代际抓拍一次贯穿全部重试轮,每轮起跳先查(陈旧/destroyed=不再开 socket,用不可重试的
|
|
117
|
-
* aborted 码即刻收敛 withRetry)。五轮复审:共享尝试零 caller 信号(取消面全在 per-caller race)。 */
|
|
118
73
|
startConnect(kind) {
|
|
119
74
|
if (this.inFlight && this.inFlight.epoch === this.connEpoch)
|
|
120
75
|
return this.inFlight.promise;
|
|
121
76
|
const epoch = this.connEpoch;
|
|
122
77
|
const policy = {
|
|
123
|
-
retryableCodes: ["connect_failed", "timeout"],
|
|
78
|
+
retryableCodes: ["connect_failed", "timeout"],
|
|
124
79
|
maxAttempts: this.cfg.retry.maxAttempts,
|
|
125
80
|
backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
|
|
126
81
|
};
|
|
@@ -135,28 +90,22 @@ export class RemoteSshExecutionEnv {
|
|
|
135
90
|
const record = { epoch, kind, promise: raw };
|
|
136
91
|
record.promise = raw.then((r) => {
|
|
137
92
|
if (this.inFlight === record)
|
|
138
|
-
this.inFlight = undefined;
|
|
93
|
+
this.inFlight = undefined;
|
|
139
94
|
return r;
|
|
140
95
|
});
|
|
141
96
|
this.inFlight = record;
|
|
142
97
|
return record.promise;
|
|
143
98
|
}
|
|
144
|
-
/** [1165] exec/execStream/sftp 共用的懒连接入口。已连=零开销直过;未连=走完整 connect()(含重试策略)。
|
|
145
|
-
* caller 的 signal 只中止**它自己的等待**(共享在飞尝试继续伺服其他等待者;彻底取消归 destroy 的代际
|
|
146
|
-
* 闸管,handshake 余命 ≤ readyTimeout 有界)。 */
|
|
147
99
|
async ensureConnected(signal) {
|
|
148
|
-
// destroyed 先于 conn 查(三轮复审:teardown 竞窗里 conn 可能在 destroy 后才被 ready 赋上——那条
|
|
149
|
-
// 连接由 ready 守卫立即 end,这里也不得放行)。
|
|
150
100
|
if (this.destroyed)
|
|
151
101
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH env destroyed — no lazy reconnect after teardown") };
|
|
152
102
|
if (signal?.aborted)
|
|
153
103
|
return { ok: false, error: new ExecutionError("aborted", "aborted before SSH lazy-connect") };
|
|
154
104
|
if (this.conn)
|
|
155
105
|
return ok(undefined);
|
|
156
|
-
const shared = this.startConnect("establish");
|
|
106
|
+
const shared = this.startConnect("establish");
|
|
157
107
|
let r;
|
|
158
108
|
if (signal) {
|
|
159
|
-
// 可中止等待:只退出本 caller 的 await,不动共享在飞记录(其他等待者照常拿结果)。
|
|
160
109
|
let onAbort;
|
|
161
110
|
const aborted = new Promise((res) => {
|
|
162
111
|
onAbort = () => res("aborted");
|
|
@@ -175,8 +124,6 @@ export class RemoteSshExecutionEnv {
|
|
|
175
124
|
if (r.ok) {
|
|
176
125
|
if (this.conn)
|
|
177
126
|
return ok(undefined);
|
|
178
|
-
// 旗到手但连接已不在(建后即断竞窗)——在飞记录 settle 时已自清;诚实报不可用,下次调用自然
|
|
179
|
-
// 重建(不递归重连:防断连风暴无界重试)。
|
|
180
127
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH connection dropped right after connect") };
|
|
181
128
|
}
|
|
182
129
|
if (r.error.code === "aborted")
|
|
@@ -184,7 +131,7 @@ export class RemoteSshExecutionEnv {
|
|
|
184
131
|
return { ok: false, error: new ExecutionError("shell_unavailable", `SSH lazy-connect failed: ${r.error.message}`, r.error) };
|
|
185
132
|
}
|
|
186
133
|
connectOnce(signal, opEpoch) {
|
|
187
|
-
const epoch = opEpoch ?? this.connEpoch;
|
|
134
|
+
const epoch = opEpoch ?? this.connEpoch;
|
|
188
135
|
return new Promise((resolve) => {
|
|
189
136
|
if (signal?.aborted)
|
|
190
137
|
return resolve({ ok: false, error: new RemoteExecutionError("aborted", "connect aborted") });
|
|
@@ -199,19 +146,15 @@ export class RemoteSshExecutionEnv {
|
|
|
199
146
|
};
|
|
200
147
|
c.on("ready", () => {
|
|
201
148
|
if (this.destroyed || epoch !== this.connEpoch) {
|
|
202
|
-
// [1165] 三轮复审 teardown/代际竞窗:destroy 或 reconnect 之后才 ready 的旧代连接——立即断掉
|
|
203
|
-
// 且不赋 conn(否则 sole destroy 之后留一条活连接+可执行面,或旧代顶掉新代)。
|
|
204
149
|
try {
|
|
205
150
|
c.end();
|
|
206
151
|
}
|
|
207
|
-
catch {
|
|
152
|
+
catch { }
|
|
208
153
|
resolve({ ok: false, error: new RemoteExecutionError("connect_failed", this.destroyed ? "SSH env destroyed while connecting" : "SSH connection superseded while connecting (reconnect)") });
|
|
209
154
|
return;
|
|
210
155
|
}
|
|
211
156
|
this.conn = c;
|
|
212
157
|
this.sftpPromise = undefined;
|
|
213
|
-
// If this live connection later drops, clear our handles so the next exec()/fs op fails closed with
|
|
214
|
-
// "shell_unavailable" (council #3) instead of hitting a dead-but-truthy client with a raw ssh2 error.
|
|
215
158
|
c.on("close", () => {
|
|
216
159
|
if (this.conn === c) {
|
|
217
160
|
this.conn = undefined;
|
|
@@ -224,7 +167,6 @@ export class RemoteSshExecutionEnv {
|
|
|
224
167
|
c.end();
|
|
225
168
|
}
|
|
226
169
|
catch {
|
|
227
|
-
/* noop */
|
|
228
170
|
}
|
|
229
171
|
resolve({ ok: false, error: this.connErr(e) });
|
|
230
172
|
});
|
|
@@ -245,29 +187,24 @@ export class RemoteSshExecutionEnv {
|
|
|
245
187
|
async postResumeInit() {
|
|
246
188
|
return unsupported("postResumeInit");
|
|
247
189
|
}
|
|
248
|
-
/** Re-establish a dropped connection (supported — unlike suspend, reconnect is idempotent establishment). */
|
|
249
190
|
async reconnect(_sessionToken) {
|
|
250
191
|
if (this.destroyed)
|
|
251
192
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "SSH env destroyed — create a fresh env instead of reviving") };
|
|
252
|
-
// 五轮复审:并发 reconnect join 已在飞的 replace 记录(不再各自 bump 代际=每路一条握手的重连风暴);
|
|
253
|
-
// 只有「无在飞 replace」才推代际顶掉旧 establish/旧连接。
|
|
254
193
|
if (this.inFlight && this.inFlight.epoch === this.connEpoch && this.inFlight.kind === "replace")
|
|
255
194
|
return this.inFlight.promise;
|
|
256
|
-
this.connEpoch++;
|
|
195
|
+
this.connEpoch++;
|
|
257
196
|
this.conn?.end();
|
|
258
197
|
this.conn = undefined;
|
|
259
198
|
this.sftpPromise = undefined;
|
|
260
|
-
return this.startConnect("replace");
|
|
199
|
+
return this.startConnect("replace");
|
|
261
200
|
}
|
|
262
|
-
/** Close the SSH connection. Does NOT destroy the machine (real system) — idempotent, never throws. */
|
|
263
201
|
async destroy() {
|
|
264
|
-
this.destroyed = true;
|
|
265
|
-
this.connEpoch++;
|
|
202
|
+
this.destroyed = true;
|
|
203
|
+
this.connEpoch++;
|
|
266
204
|
try {
|
|
267
205
|
this.conn?.end();
|
|
268
206
|
}
|
|
269
207
|
catch {
|
|
270
|
-
/* best-effort */
|
|
271
208
|
}
|
|
272
209
|
this.conn = undefined;
|
|
273
210
|
this.sftpPromise = undefined;
|
|
@@ -275,10 +212,7 @@ export class RemoteSshExecutionEnv {
|
|
|
275
212
|
async cleanup() {
|
|
276
213
|
return this.destroy();
|
|
277
214
|
}
|
|
278
|
-
// ─────────────────────────────── exec ───────────────────────────────
|
|
279
215
|
async exec(command, options) {
|
|
280
|
-
// [1165] lazy-connect:core prepare-task 铸 env 不显式 connect(k8s 道同前提)——首用自动建连;
|
|
281
|
-
// caller 的 abortSignal 覆盖建连等待段(pre-abort 与等待中 abort 都即时退出)。
|
|
282
216
|
const ready = await this.ensureConnected(options?.abortSignal);
|
|
283
217
|
if (!ready.ok)
|
|
284
218
|
return ready;
|
|
@@ -287,10 +221,6 @@ export class RemoteSshExecutionEnv {
|
|
|
287
221
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH not connected — call connect() first") };
|
|
288
222
|
const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
|
|
289
223
|
return new Promise((resolve) => {
|
|
290
|
-
// D2: bounded rolling-TAIL (core's RollingTailBuffer) so a model-driven `yes`/`cat big` can't OOM the
|
|
291
|
-
// durable worker — keep only the last 8MB (tail = errors+exit) + a dropped-byte marker, O(maxBytes) memory.
|
|
292
|
-
// SAFE: this is the model-facing TEXT exec path only; readBinaryFile uses SFTP (sftp.readFile), a separate
|
|
293
|
-
// byte-exact path that NEVER flows through these buffers.
|
|
294
224
|
const outBuf = new RollingTailBuffer();
|
|
295
225
|
const errBuf = new RollingTailBuffer();
|
|
296
226
|
let settled = false;
|
|
@@ -303,18 +233,14 @@ export class RemoteSshExecutionEnv {
|
|
|
303
233
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
304
234
|
conn.removeListener("close", onConnClose);
|
|
305
235
|
try {
|
|
306
|
-
streamRef?.close();
|
|
236
|
+
streamRef?.close();
|
|
307
237
|
}
|
|
308
238
|
catch {
|
|
309
|
-
/* noop */
|
|
310
239
|
}
|
|
311
240
|
resolve(r);
|
|
312
241
|
};
|
|
313
242
|
const timer = setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `ssh exec exceeded ${timeoutMs}ms (provider/command hang)`) }), timeoutMs);
|
|
314
243
|
const onAbort = () => finish({ ok: false, error: new ExecutionError("aborted", "ssh exec aborted") });
|
|
315
|
-
// Transport mapping: the TCP/SSH transport dying mid-command must NOT look like success or a vague
|
|
316
|
-
// failure. Typed "transport_lost" since core 1.89 (outcome UNKNOWN — reconnect, then retry
|
|
317
|
-
// only per the op's idempotency); the close-without-exit-status path below catches the same condition.
|
|
318
244
|
const onConnClose = () => finish({ ok: false, error: new ExecutionError("transport_lost", "ssh transport lost mid-command (connection closed before exit status) — reconnect + retry per idempotency") });
|
|
319
245
|
conn.once("close", onConnClose);
|
|
320
246
|
if (options?.abortSignal) {
|
|
@@ -329,30 +255,23 @@ export class RemoteSshExecutionEnv {
|
|
|
329
255
|
streamRef = stream;
|
|
330
256
|
if (settled) {
|
|
331
257
|
try {
|
|
332
|
-
stream.close();
|
|
258
|
+
stream.close();
|
|
333
259
|
}
|
|
334
260
|
catch {
|
|
335
|
-
/* noop */
|
|
336
261
|
}
|
|
337
262
|
return;
|
|
338
263
|
}
|
|
339
264
|
stream
|
|
340
265
|
.on("data", (d) => {
|
|
341
|
-
outBuf.push(d);
|
|
266
|
+
outBuf.push(d);
|
|
342
267
|
options?.onStdout?.(d.toString());
|
|
343
268
|
})
|
|
344
269
|
.on("close", (code, signal) => {
|
|
345
|
-
// code==null without a kill signal = the channel died without delivering an exit status (transport drop),
|
|
346
|
-
// NOT a successful exit — was silently mapped to exitCode 0 before ([R78]#1).
|
|
347
270
|
if (code == null && !signal) {
|
|
348
271
|
return finish({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") });
|
|
349
272
|
}
|
|
350
273
|
const out = outBuf.result();
|
|
351
274
|
const err = errBuf.result();
|
|
352
|
-
// code==null WITH a signal = the remote command was killed by that signal — conventional 128+signo,
|
|
353
|
-
// NOT a fake success 0 ([R78]#2; was silently mapped to exitCode 0 before this fix). The `: 1`
|
|
354
|
-
// arm is unreachable (the transport_lost return above already covers code==null && !signal) but
|
|
355
|
-
// keeps the type honest without a non-null assertion.
|
|
356
275
|
const exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 1);
|
|
357
276
|
finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
|
|
358
277
|
})
|
|
@@ -363,9 +282,8 @@ export class RemoteSshExecutionEnv {
|
|
|
363
282
|
});
|
|
364
283
|
});
|
|
365
284
|
}
|
|
366
|
-
// ─────────────────────────────── streaming exec ───────────────────────────────
|
|
367
285
|
async *execStream(command, options) {
|
|
368
|
-
const ready = await this.ensureConnected(options?.signal);
|
|
286
|
+
const ready = await this.ensureConnected(options?.signal);
|
|
369
287
|
if (!ready.ok)
|
|
370
288
|
throw new RemoteExecutionError(ready.error.code === "aborted" ? "aborted" : "connect_failed", ready.error.message, ready.error);
|
|
371
289
|
const conn = this.conn;
|
|
@@ -386,9 +304,7 @@ export class RemoteSshExecutionEnv {
|
|
|
386
304
|
let streamRef;
|
|
387
305
|
let onAbort;
|
|
388
306
|
let paused = false;
|
|
389
|
-
let done = false;
|
|
390
|
-
// [R78]#1: the SSH transport dying mid-stream is a typed, retryable condition — map to "transport_lost"
|
|
391
|
-
// (in the upper layer's retry allowlist) instead of a silent finish or "unknown".
|
|
307
|
+
let done = false;
|
|
392
308
|
const onConnClose = () => {
|
|
393
309
|
if (!finished) {
|
|
394
310
|
failure = failure ?? new RemoteExecutionError("transport_lost", "ssh transport lost mid-stream (connection closed) — reconnect + retry per idempotency");
|
|
@@ -398,8 +314,6 @@ export class RemoteSshExecutionEnv {
|
|
|
398
314
|
};
|
|
399
315
|
conn.once("close", onConnClose);
|
|
400
316
|
const teardown = () => {
|
|
401
|
-
// runs on EVERY exit path (normal end, idle-timeout/failure throw, AND consumer early-break via the generator's
|
|
402
|
-
// .return()): drop the abort listener (council #5) + close the channel (council #2 idle-timeout leak).
|
|
403
317
|
done = true;
|
|
404
318
|
conn.removeListener("close", onConnClose);
|
|
405
319
|
if (onAbort) {
|
|
@@ -410,7 +324,6 @@ export class RemoteSshExecutionEnv {
|
|
|
410
324
|
streamRef?.close();
|
|
411
325
|
}
|
|
412
326
|
catch {
|
|
413
|
-
/* noop */
|
|
414
327
|
}
|
|
415
328
|
};
|
|
416
329
|
const full = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
@@ -424,10 +337,9 @@ export class RemoteSshExecutionEnv {
|
|
|
424
337
|
streamRef = stream;
|
|
425
338
|
if (done) {
|
|
426
339
|
try {
|
|
427
|
-
stream.close();
|
|
340
|
+
stream.close();
|
|
428
341
|
}
|
|
429
342
|
catch {
|
|
430
|
-
/* noop */
|
|
431
343
|
}
|
|
432
344
|
return;
|
|
433
345
|
}
|
|
@@ -446,7 +358,7 @@ export class RemoteSshExecutionEnv {
|
|
|
446
358
|
lastChunk = Date.now();
|
|
447
359
|
queue.push(chunk);
|
|
448
360
|
if (queue.length >= 512 && !paused) {
|
|
449
|
-
paused = true;
|
|
361
|
+
paused = true;
|
|
450
362
|
stream.pause();
|
|
451
363
|
}
|
|
452
364
|
signalReady();
|
|
@@ -455,11 +367,8 @@ export class RemoteSshExecutionEnv {
|
|
|
455
367
|
.on("data", (d) => enqueue({ type: "stdout", data: d.toString() }))
|
|
456
368
|
.on("close", (code, signal) => {
|
|
457
369
|
if (code == null && !signal && !failure) {
|
|
458
|
-
// channel died without an exit status (transport drop) — typed retryable, not a fake exit 0 ([R78]#1)
|
|
459
370
|
failure = new RemoteExecutionError("transport_lost", "ssh channel closed without exit status (transport lost)");
|
|
460
371
|
}
|
|
461
|
-
// code==null WITH a signal = the remote command was killed by that signal — conventional 128+signo,
|
|
462
|
-
// NOT a fake success 0 ([R78]#2; was silently mapped to exitCode 0 before this fix).
|
|
463
372
|
exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 0);
|
|
464
373
|
finished = true;
|
|
465
374
|
signalReady();
|
|
@@ -478,9 +387,7 @@ export class RemoteSshExecutionEnv {
|
|
|
478
387
|
throw failure;
|
|
479
388
|
if (finished)
|
|
480
389
|
break;
|
|
481
|
-
// idle/liveness bound: no chunk within readTimeoutMs → typed timeout (unreachable host)
|
|
482
390
|
const waitMs = idleMs ? Math.max(1, idleMs - (Date.now() - lastChunk)) : 30_000;
|
|
483
|
-
// [4947] A-002.11 sibling: clear the losing timer — wake() firing first used to leave it armed.
|
|
484
391
|
let waitTimer;
|
|
485
392
|
try {
|
|
486
393
|
await new Promise((r) => {
|
|
@@ -503,11 +410,8 @@ export class RemoteSshExecutionEnv {
|
|
|
503
410
|
teardown();
|
|
504
411
|
}
|
|
505
412
|
}
|
|
506
|
-
// ─────────────────────────────── FileSystem (sftp + shell hybrid) ───────────────────────────────
|
|
507
413
|
async sftp(abortSignal) {
|
|
508
|
-
const ready = await this.ensureConnected(abortSignal);
|
|
509
|
-
// 四轮复审:取消跨 sftp 边界保码——aborted 直铸 FileError("aborted")(fileError() 对 FileError 原样
|
|
510
|
-
// 放行),不得降级成 "unknown" 触发上游误重试。
|
|
414
|
+
const ready = await this.ensureConnected(abortSignal);
|
|
511
415
|
if (!ready.ok)
|
|
512
416
|
return Promise.reject(ready.error.code === "aborted" ? new FileError("aborted", ready.error.message) : new Error(ready.error.message));
|
|
513
417
|
if (!this.conn)
|
|
@@ -553,14 +457,9 @@ export class RemoteSshExecutionEnv {
|
|
|
553
457
|
}
|
|
554
458
|
async writeFile(p, content, abortSignal) {
|
|
555
459
|
const abs = this.resolve(p);
|
|
556
|
-
// sftp.writeFile does NOT create parent dirs → mkdir -p the parent first (parity with E2B's auto-create).
|
|
557
460
|
const dir = path.posix.dirname(abs);
|
|
558
461
|
if (dir && dir !== "." && dir !== "/") {
|
|
559
462
|
const mk = await this.exec(`mkdir -p -- ${shellQuote(dir)}`, { abortSignal });
|
|
560
|
-
// 🔴 `!mk.ok` 也必须 fail-closed(2026-07-25 补):原来只看 `exitCode !== 0`,于是 **exec 本身失败**
|
|
561
|
-
// (设备/连接在这一拍掉线 ⇒ `transport_lost` / `shell_unavailable`)被整条吞掉,后面照样推载荷,
|
|
562
|
-
// 而整个 `writeFile` 返回 **ok:true** —— 调用方看到的成功与真实故障点完全脱节。同族的
|
|
563
|
-
// local-docker / k8s 用的正是 `if (!mk.ok) return`,这两条腿是漏的。
|
|
564
463
|
if (!mk.ok)
|
|
565
464
|
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
566
465
|
if (mk.value.exitCode !== 0)
|
|
@@ -609,7 +508,7 @@ export class RemoteSshExecutionEnv {
|
|
|
609
508
|
const sftp = await this.sftp(abortSignal);
|
|
610
509
|
const entries = await new Promise((resolve, reject) => sftp.readdir(abs, (err, list) => (err ? reject(err) : resolve(list))));
|
|
611
510
|
return ok(entries.map((e) => {
|
|
612
|
-
const a = e.attrs;
|
|
511
|
+
const a = e.attrs;
|
|
613
512
|
return { name: e.filename, path: path.posix.join(abs, e.filename), kind: kindFromMode(a.mode), size: a.size ?? 0, mtimeMs: (a.mtime ?? 0) * 1000 };
|
|
614
513
|
}));
|
|
615
514
|
}
|
|
@@ -617,13 +516,12 @@ export class RemoteSshExecutionEnv {
|
|
|
617
516
|
return { ok: false, error: this.fileError(e, abs) };
|
|
618
517
|
}
|
|
619
518
|
}
|
|
620
|
-
// ── design/158 B6:shell-out 文件面委托共享基座(posix-shell-fs.ts;fileInfo/listDir/exists 走 SFTP 原生,不并)──
|
|
621
519
|
posixFsInst;
|
|
622
520
|
get posixFs() {
|
|
623
521
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
624
522
|
run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
|
|
625
523
|
resolve: (p) => this.resolve(p),
|
|
626
|
-
tempDir: () => null,
|
|
524
|
+
tempDir: () => null,
|
|
627
525
|
}));
|
|
628
526
|
}
|
|
629
527
|
async readLink(p, abortSignal) {
|
|
@@ -652,7 +550,6 @@ export class RemoteSshExecutionEnv {
|
|
|
652
550
|
async createTempFile(options) {
|
|
653
551
|
return this.posixFs.createTempFile(options);
|
|
654
552
|
}
|
|
655
|
-
// ─────────────────────────────── helpers ───────────────────────────────
|
|
656
553
|
withCwdEnv(command, cwd, env) {
|
|
657
554
|
const parts = [];
|
|
658
555
|
const dir = cwd ?? (this.cwd !== "." ? this.cwd : undefined);
|
|
@@ -667,7 +564,7 @@ export class RemoteSshExecutionEnv {
|
|
|
667
564
|
connErr(e) {
|
|
668
565
|
const msg = e.message || String(e);
|
|
669
566
|
if (e.level === "client-authentication" || /authentication|all configured authentication methods failed/i.test(msg)) {
|
|
670
|
-
return new RemoteExecutionError("auth_failed", `ssh auth failed: ${msg}`, e);
|
|
567
|
+
return new RemoteExecutionError("auth_failed", `ssh auth failed: ${msg}`, e);
|
|
671
568
|
}
|
|
672
569
|
if (e.level === "client-timeout" || /timed out|timeout/i.test(msg))
|
|
673
570
|
return new RemoteExecutionError("timeout", `ssh connect timed out: ${msg}`, e);
|
|
@@ -689,11 +586,6 @@ export class RemoteSshExecutionEnv {
|
|
|
689
586
|
return new FileError("unknown", msg, p);
|
|
690
587
|
}
|
|
691
588
|
}
|
|
692
|
-
/**
|
|
693
|
-
* `ExecutionEnvFactory` for the SSH backend — one fresh (unconnected) env per task against the same host
|
|
694
|
-
* (lazy: connects on first use). Config (host/user/key) is control-plane-supplied; the Runner owns the
|
|
695
|
-
* lifetime and calls `destroy()` (= disconnect) on task end.
|
|
696
|
-
*/
|
|
697
589
|
export function sshExecutionEnvFactory(config) {
|
|
698
590
|
return (_ctx) => new RemoteSshExecutionEnv(config);
|
|
699
591
|
}
|
|
@@ -1,46 +1,26 @@
|
|
|
1
|
-
/** The remote lanes that get the in-sandbox scratchpad convention. adb (no standard /tmp) and host
|
|
2
|
-
* (worker-local scratchpad already exists) are deliberately absent. */
|
|
3
1
|
const REMOTE_SCRATCHPAD_LANES = new Set(["e2b", "k8s", "local-docker", "ssh"]);
|
|
4
2
|
export function isRemoteScratchpadLane(provider) {
|
|
5
3
|
return provider !== undefined && REMOTE_SCRATCHPAD_LANES.has(provider);
|
|
6
4
|
}
|
|
7
|
-
/** Charset gate for embedding the sessionId in a shell command. Dots allowed (unlike the host-lane segment
|
|
8
|
-
* sanitizer, which REWRITES): here an id either passes verbatim or the feature stays off. "/" can't pass, so a
|
|
9
|
-
* multi-segment escape can't — but the two SINGLE-segment escapes `.` (= the shared scratchpad root) and `..`
|
|
10
|
-
* (= /tmp itself) DO match the charset and are rejected explicitly below (codex HIGH: an opaque caller-chosen
|
|
11
|
-
* sessionId of exactly "."/".." would alias another session's root on a shared ssh host). */
|
|
12
5
|
const SESSION_ID_RE = /^[A-Za-z0-9._-]+$/;
|
|
13
|
-
/** THE remote scratchpad path convention: `/tmp/scratchpad/<sessionId>` (raw id — the sandbox is already
|
|
14
|
-
* session/task-scoped tenancy, no hash segment needed). Returns undefined for an absent/unsafe id — the
|
|
15
|
-
* single validity rule shared by the factory decorator AND the envFacts advertisement. */
|
|
16
6
|
export function remoteScratchpadDirFor(sessionId) {
|
|
17
7
|
if (!sessionId || sessionId === "." || sessionId === ".." || !SESSION_ID_RE.test(sessionId))
|
|
18
8
|
return undefined;
|
|
19
9
|
return `/tmp/scratchpad/${sessionId}`;
|
|
20
10
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Decorate a factory so each minted env lazily `mkdir -p`s its session scratchpad before its FIRST exec.
|
|
23
|
-
* The env is mutated IN PLACE (rebind `env.exec`, evictLspOnDestroy precedent) — a copy would drop the
|
|
24
|
-
* adapter's prototype methods and identity (registries key on the instance).
|
|
25
|
-
*/
|
|
26
11
|
export function withRemoteScratchpad(factory, logger) {
|
|
27
12
|
return async (ctx) => {
|
|
28
13
|
const env = await factory(ctx);
|
|
29
14
|
const dir = remoteScratchpadDirFor(ctx.sessionId);
|
|
30
15
|
if (dir === undefined) {
|
|
31
|
-
// absent id (adhoc no-session spawn) = silently no scratchpad; a NON-empty id failing the charset gate
|
|
32
|
-
// is a wiring anomaly worth a warn (our ids are uuids — this should never fire outside a bug).
|
|
33
16
|
if (ctx.sessionId)
|
|
34
17
|
logger?.warn?.("remote_scratchpad_skipped_unsafe_session_id", { sessionId: ctx.sessionId });
|
|
35
18
|
return env;
|
|
36
19
|
}
|
|
37
20
|
const e = env;
|
|
38
21
|
if (typeof e.exec !== "function")
|
|
39
|
-
return env;
|
|
22
|
+
return env;
|
|
40
23
|
const origExec = e.exec.bind(e);
|
|
41
|
-
// Single-flight via a cached PROMISE (not a boolean): concurrent first commands await the SAME mkdir.
|
|
42
|
-
// The promise never rejects (failure is swallowed into the warn) and stays cached once settled — mkdir
|
|
43
|
-
// runs AT MOST once per env, success or not (best-effort posture, see file header).
|
|
44
24
|
let ensured;
|
|
45
25
|
const ensure = () => (ensured ??= (async () => {
|
|
46
26
|
try {
|
|
@@ -57,17 +37,11 @@ export function withRemoteScratchpad(factory, logger) {
|
|
|
57
37
|
}
|
|
58
38
|
})());
|
|
59
39
|
e.exec = async (command, options) => {
|
|
60
|
-
// Pre-aborted call must NOT trigger the mkdir prelude: on a lazy-VM lane the adapter's own exec guard
|
|
61
|
-
// refuses BEFORE provisioning, but our optionless mkdir would bypass that guard and force-boot a billable
|
|
62
|
-
// VM just to abort (codex M2). Mid-mkdir aborts stay coarse (the command is trivial) — accepted.
|
|
63
40
|
if (options?.abortSignal?.aborted)
|
|
64
41
|
return origExec(command, options);
|
|
65
42
|
await ensure();
|
|
66
43
|
return origExec(command, options);
|
|
67
44
|
};
|
|
68
|
-
// Background leg (codex M1): core's background path calls `spawnBackground`, NOT `exec` — a task whose FIRST
|
|
69
|
-
// command is a bg spawn must ride the SAME single-flight mkdir or its scratchpad write fails. Duck-typed (the
|
|
70
|
-
// capability is optional per hasBackgroundShell); BackgroundSpawnOptions has no abortSignal, so no pre-abort leg.
|
|
71
45
|
const bgHost = e;
|
|
72
46
|
if (typeof bgHost.spawnBackground === "function") {
|
|
73
47
|
const origSpawn = bgHost.spawnBackground.bind(bgHost);
|
|
@@ -76,16 +50,12 @@ export function withRemoteScratchpad(factory, logger) {
|
|
|
76
50
|
return origSpawn(command, options);
|
|
77
51
|
};
|
|
78
52
|
}
|
|
79
|
-
// PAIR-REVIEW F-5:canonicalPath 腿**显式**接 ensure——core 1.351 的 prepare 期 scratchpad 实体化/
|
|
80
|
-
// additionalRoots canonicalize 走 env.canonicalPath;local-docker 的实现私径直调 docker(绕 exec 包装)
|
|
81
|
-
// → 目录未建 not_found → 围栏不 admit(广告了但 Write 恒拒,[1339] 原病残留)。e2b/ssh/k8s 此前只是
|
|
82
|
-
// **恰好**内部走 this.exec 被顺带救活(巧合承重)——此腿把六 adapter 拉到同一显式契约。
|
|
83
53
|
const cpHost = e;
|
|
84
54
|
if (typeof cpHost.canonicalPath === "function") {
|
|
85
55
|
const origCanonical = cpHost.canonicalPath.bind(cpHost);
|
|
86
56
|
cpHost.canonicalPath = async (p, abortSignal) => {
|
|
87
57
|
if (abortSignal?.aborted)
|
|
88
|
-
return origCanonical(p, abortSignal);
|
|
58
|
+
return origCanonical(p, abortSignal);
|
|
89
59
|
await ensure();
|
|
90
60
|
return origCanonical(p, abortSignal);
|
|
91
61
|
};
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* D5 pipe-destroy grace (parity with core's vendored `EXEC_FORCE_SETTLE_GRACE_MS`, harness/env/nodejs.js):
|
|
3
|
-
* after a `kill()` on an abort/timeout, the child's `close` may NEVER fire — a re-parented grandchild that
|
|
4
|
-
* inherited the stdout/stderr pipe (or a D-state process) can hold those FDs open indefinitely. Our buffered
|
|
5
|
-
* exec already SETTLES the promise synchronously on abort/timeout (no hang), but the dangling pipe FDs leak.
|
|
6
|
-
* So after a kill we arm a short grace timer; if `close` hasn't fired by then we `destroy()` the pipes to
|
|
7
|
-
* force the FDs released. Core does NOT export this const (it's a vendor-local value), so service defines its
|
|
8
|
-
* own — a plain number, not a security/behavioural boundary.
|
|
9
|
-
*/
|
|
10
1
|
export const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
|
|
11
|
-
/**
|
|
12
|
-
* D5 helper: arm a one-shot grace timer that `destroy()`s {@link ChildProcess.stdout}/`stderr` if the child's
|
|
13
|
-
* `close` never lands after a kill (a re-parented grandchild holding the pipe → dangling FD). Returns the
|
|
14
|
-
* timer so the caller can {@link clearTimeout} it the moment a real `close` (or any settle) arrives — the timer
|
|
15
|
-
* is `unref`'d so it can never keep the process alive on its own. This ONLY frees pipe FDs; it does NOT settle
|
|
16
|
-
* any promise (the buffered exec already settled synchronously on abort/timeout — that path is unchanged).
|
|
17
|
-
*/
|
|
18
2
|
export function armPipeDestroyGrace(child) {
|
|
19
3
|
const t = setTimeout(() => {
|
|
20
4
|
try {
|
|
@@ -22,18 +6,14 @@ export function armPipeDestroyGrace(child) {
|
|
|
22
6
|
child.stderr?.destroy();
|
|
23
7
|
}
|
|
24
8
|
catch {
|
|
25
|
-
/* best-effort pipe teardown — never throw from a timer */
|
|
26
9
|
}
|
|
27
10
|
}, EXEC_FORCE_SETTLE_GRACE_MS);
|
|
28
11
|
t.unref?.();
|
|
29
12
|
return t;
|
|
30
13
|
}
|
|
31
|
-
/** POSIX single-quote a string for safe interpolation into a remote shell command (the only metacharacter
|
|
32
|
-
* inside '' is ' itself, closed/escaped/reopened). */
|
|
33
14
|
export function shellQuote(s) {
|
|
34
15
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
35
16
|
}
|
|
36
|
-
/** Derive FileInfo kind from POSIX st_mode bits (raw stat/readdir attrs carry no isDirectory() helper). */
|
|
37
17
|
export function kindFromMode(mode) {
|
|
38
18
|
const t = (mode ?? 0) & 0o170000;
|
|
39
19
|
if (t === 0o040000)
|
|
@@ -42,13 +22,6 @@ export function kindFromMode(mode) {
|
|
|
42
22
|
return "symlink";
|
|
43
23
|
return "file";
|
|
44
24
|
}
|
|
45
|
-
// ── design/158 B4 交接小件:plugins 数值旋钮的 fail-loud ─────────────────────────────────────────────
|
|
46
|
-
// HOST_BG_*/K8S_BG_*/E2B_* 是模块级、import 时读的常量,不走 ServiceConfig;此前手搓
|
|
47
|
-
// `Math.max(min, Math.floor(Number(env ?? "")) || def)` 让非法字面量(`1mb`)静默落默认——与 config.ts
|
|
48
|
-
// boolEnv 修掉的同族病。告警收本模块自带通道(不造 plugins→config 依赖边),main.ts 在 boot 诊断点
|
|
49
|
-
// (logConfigDiagnostics 旁)排空——ESM import 先于 main 体执行,时序成立。
|
|
50
|
-
// 语义逐字承原形:unset/"" 静默默认;显式 0 仍落默认(0 在这族旋钮上无合法语义,关功能走 *_ENABLED);
|
|
51
|
-
// 合法值 floor+min 夹紧。唯一新增行为=非法字面量推一条警告。
|
|
52
25
|
const NUM_ENV_WARNINGS = [];
|
|
53
26
|
const NUM_ENV_SEEN = new Set();
|
|
54
27
|
export function drainNumEnvWarnings() {
|
|
@@ -57,12 +30,8 @@ export function drainNumEnvWarnings() {
|
|
|
57
30
|
export function numEnvOr(name, def, min) {
|
|
58
31
|
const raw = process.env[name] ?? "";
|
|
59
32
|
const n = Math.floor(Number(raw));
|
|
60
|
-
// A-067 C1-1(F2a):判据必须是 isFinite,不是 isNaN——`"Infinity"`/`"1e999"` 穿过 NaN 检查后
|
|
61
|
-
// `n || def` 取 Infinity、`Math.max(min, Infinity)` 恒 Infinity ⇒ 本函数独供的 17 个执行 lane
|
|
62
|
-
// 旋钮里,E2B_SANDBOX_MAX_MS 的消费点 `Math.min(callerDeadline, cap)`(BL-31 防挂钳)被整条击穿。
|
|
63
|
-
// 非有限=非法字面量同待遇:警告 + 落默认。
|
|
64
33
|
if (raw !== "" && !Number.isFinite(n) && !NUM_ENV_SEEN.has(name)) {
|
|
65
|
-
NUM_ENV_SEEN.add(name);
|
|
34
|
+
NUM_ENV_SEEN.add(name);
|
|
66
35
|
NUM_ENV_WARNINGS.push({ env: name, raw });
|
|
67
36
|
}
|
|
68
37
|
return Math.max(min, (Number.isFinite(n) ? n : 0) || def);
|
|
@@ -1,46 +1,35 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
2
|
-
/** Dual-dialect durable resume-at anchor map. See the file header for the dialect-delta ledger. */
|
|
3
2
|
export class SqlResumeAnchorStore {
|
|
4
3
|
db;
|
|
5
4
|
constructor(db) {
|
|
6
5
|
this.db = db;
|
|
7
6
|
}
|
|
8
|
-
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
9
7
|
q(tidb, pg) {
|
|
10
8
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
11
9
|
}
|
|
12
|
-
/** Record/overwrite the anchor for one settled message boundary (idempotent upsert on the (session_id,event_id) PK). */
|
|
13
10
|
async put(sessionId, eventId, entryId, owner) {
|
|
14
11
|
await this.db.query(this.q("INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES (?,?,?,?,?) " +
|
|
15
12
|
"ON DUPLICATE KEY UPDATE entry_id = VALUES(entry_id), owner = VALUES(owner)", "INSERT INTO resume_anchor (session_id, event_id, entry_id, owner, created_at) VALUES ($1,$2,$3,$4,$5) " +
|
|
16
13
|
"ON CONFLICT (session_id, event_id) DO UPDATE SET entry_id = EXCLUDED.entry_id, owner = EXCLUDED.owner"), [sessionId, eventId, entryId, owner, new Date()]);
|
|
17
14
|
}
|
|
18
|
-
/** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id; undefined = unknown anchor. */
|
|
19
15
|
async resolve(sessionId, eventId, owner) {
|
|
20
16
|
const { rows } = await this.db.query(this.q("SELECT entry_id FROM resume_anchor WHERE session_id = ? AND event_id = ? AND owner <=> ?", "SELECT entry_id FROM resume_anchor WHERE session_id = $1 AND event_id = $2 AND owner IS NOT DISTINCT FROM $3"), [sessionId, eventId, owner]);
|
|
21
17
|
return rows[0] ? String(rows[0].entry_id) : undefined;
|
|
22
18
|
}
|
|
23
|
-
/** 2c session-sync: ALL anchors for a session (every eventId→entryId + its owner), UNFILTERED — a cross-backend
|
|
24
|
-
* session EXPORT bundles the whole anchor map so the importer can RE-KEY owner to the target principal (§9). The
|
|
25
|
-
* route already proved session ownership (the export gate), so no per-row owner filter here. owner carried verbatim. */
|
|
26
19
|
async listBySession(sessionId) {
|
|
27
20
|
const { rows } = await this.db.query(this.q("SELECT event_id, entry_id, owner FROM resume_anchor WHERE session_id = ?", "SELECT event_id, entry_id, owner FROM resume_anchor WHERE session_id = $1"), [sessionId]);
|
|
28
21
|
return rows.map((r) => ({ eventId: String(r.event_id), entryId: String(r.entry_id), owner: r.owner == null ? null : String(r.owner) }));
|
|
29
22
|
}
|
|
30
|
-
/** E21 — purge ALL anchors for a session, scoped by session_id ALONE (the route proved ownership; a per-row owner
|
|
31
|
-
* guard would orphan rows whose per-run principal diverged from the session owner). Idempotent (affected count). */
|
|
32
23
|
async deleteBySession(sessionId) {
|
|
33
24
|
const res = await this.db.query(this.q("DELETE FROM resume_anchor WHERE session_id = ?", "DELETE FROM resume_anchor WHERE session_id = $1"), [sessionId]);
|
|
34
25
|
return res.affected;
|
|
35
26
|
}
|
|
36
27
|
}
|
|
37
|
-
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
38
28
|
export class TiDBResumeAnchorStore extends SqlResumeAnchorStore {
|
|
39
29
|
constructor(pool) {
|
|
40
30
|
super(mysqlDriver(pool));
|
|
41
31
|
}
|
|
42
32
|
}
|
|
43
|
-
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
44
33
|
export class PgResumeAnchorStore extends SqlResumeAnchorStore {
|
|
45
34
|
constructor(pool) {
|
|
46
35
|
super(pgDriver(pool));
|