@sema-agent/server 1.323.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +995 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +585 -1
- package/dist/config.d.ts +78 -2
- package/dist/config.js +874 -301
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +530 -2129
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +58 -2
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RemoteContainerExecutionEnv` — the E2B implementation of core's `RemoteExecutionEnv` seam
|
|
3
|
+
* (core src/core/remote-env.ts, design/48 §5). The service control plane owns this backend; core
|
|
4
|
+
* owns the seam shape (exactly like `CheckpointStore` / `SqlExecutor`).
|
|
5
|
+
*
|
|
6
|
+
* **Scope = design/48 §0.5 v1 minimal slice**: single worker, E2B managed, **Tier 2 only** (every
|
|
7
|
+
* op runs in a per-task E2B Firecracker VM — no tiering/classification), and the ~4 live seam methods
|
|
8
|
+
* `connect` / `execStream` / `workspaceHandle` / `destroy` + the base `ExecutionEnv` (FileSystem + Shell).
|
|
9
|
+
*
|
|
10
|
+
* **v1.5 (design/49, core 1.69):** `suspendVM` (E2B `pause()`) / `resumeVM` (`Sandbox.connect(id)` auto-resume)
|
|
11
|
+
* / `postResumeInit` (reachability + at-rest-encryption red line) are IMPLEMENTED — core orchestrates them when
|
|
12
|
+
* a task with this remote env hits a durable-approval `ask` (workspaceHandle → Checkpoint.state). `reconnect`
|
|
13
|
+
* (cross-replica re-attach to a still-running VM) stays deferred to **v2** (v1.5 always snapshots → resumeVM).
|
|
14
|
+
*
|
|
15
|
+
* **Provider seam discipline (self-host endgame):** every E2B SDK call is confined to this one class. A
|
|
16
|
+
* future OSS-self-hosted E2B (clay's "最终自部署" constraint) swaps the backend without touching consumers.
|
|
17
|
+
*
|
|
18
|
+
* **never-throw invariant:** the base `ExecutionEnv` contract requires every FileSystem/Shell method to
|
|
19
|
+
* encode failure in the returned `Result` (never throw). The one exception is `execStream`, whose async
|
|
20
|
+
* iterator MAY throw `RemoteExecutionError` mid-stream before an `exit` chunk (seam contract, OutputChunk).
|
|
21
|
+
*/
|
|
1
22
|
import path from "node:path";
|
|
2
23
|
import { Sandbox, CommandExitError, FileType } from "e2b";
|
|
3
24
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
@@ -5,32 +26,91 @@ import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
|
5
26
|
import { numEnvOr } from "./remote-shell.js";
|
|
6
27
|
import { BackgroundShellManager } from "./background-shell-support.js";
|
|
7
28
|
const PROVIDER = "e2b";
|
|
29
|
+
/** E2B default sandbox user home — the workspace root unless overridden. */
|
|
8
30
|
const DEFAULT_MOUNT_PATH = "/home/user";
|
|
31
|
+
/** Default sandbox lifetime if the caller does not set one (E2B bills for this window). */
|
|
9
32
|
const DEFAULT_TIMEOUT_MS = 5 * 60_000;
|
|
33
|
+
/**
|
|
34
|
+
* 🔴 BL-31 — the E2B sandbox lifetime (`Sandbox.create({ timeoutMs })`) is bounded by E2B's per-sandbox MAX
|
|
35
|
+
* (≈1h on the fleet plan). A caller that passes a long deadline as the sandbox lifetime — the static-lane leader
|
|
36
|
+
* forwards `leaderTimeoutMs` (default 24h, the leader's OVERALL deadline) straight through — makes `create()`
|
|
37
|
+
* reject and `connect()` fail (found live: canary leader judare, 2026-06-17). Clamp the requested lifetime to
|
|
38
|
+
* this max (env-tunable `E2B_SANDBOX_MAX_MS`, default 1h) — the sandbox lifetime is decoupled from the leader's
|
|
39
|
+
* overall deadline. A run longer than the clamp needs a sandbox keep-alive/extend (`setTimeout`, follow-up);
|
|
40
|
+
* today an unclamped 24h simply never connects, so a clamp is strictly safer.
|
|
41
|
+
*/
|
|
10
42
|
const E2B_SANDBOX_MAX_MS = numEnvOr("E2B_SANDBOX_MAX_MS", 3_600_000, 60_000);
|
|
43
|
+
/**
|
|
44
|
+
* 🔴 Class-fix for the E2B-hang bug (core timeout/liveness contract). Three SEMANTICALLY DISTINCT
|
|
45
|
+
* timeouts that were previously conflated into the one sandbox-lifetime value:
|
|
46
|
+
* - {@link DEFAULT_RPC_TIMEOUT_MS} — wall-clock bound for a CONTROL-PLANE RPC (create/resume/pause/kill/isRunning/
|
|
47
|
+
* files.*). These are "should-be-fast" ops; a provider hang here used to wait the whole lifetime (~30 min).
|
|
48
|
+
* - {@link DEFAULT_LIVENESS_MS} — idle/liveness bound for a COMMAND (exec/execStream): max time with NO output
|
|
49
|
+
* AND not yet complete = suspected hang. The precise hang-killer: only kills "zero-progress",
|
|
50
|
+
* never a slow-but-streaming `npm ci`. MUST also cover the command-CREATION RPC (hang happens before chunk #1).
|
|
51
|
+
* - per-command wall-clock = the caller's explicit `timeout` (slow-but-progressing cap), which MUST NOT fall back
|
|
52
|
+
* to the sandbox lifetime. Unset → liveness is the safety net, not the lifetime.
|
|
53
|
+
*/
|
|
11
54
|
const DEFAULT_RPC_TIMEOUT_MS = 60_000;
|
|
12
55
|
const DEFAULT_LIVENESS_MS = 120_000;
|
|
56
|
+
/**
|
|
57
|
+
* Wall-clock for DATA-TRANSFER / provisioning RPCs (large `files.read`/`files.write`, `suspendVM`/`resumeVM`
|
|
58
|
+
* snapshot, sandbox `create`/`connect`). 🔴 These legitimately run MINUTES — a short fixed
|
|
59
|
+
* wall-clock would kill the durable-checkpoint path (suspending a large workspace). E2B's file/lifecycle SDK
|
|
60
|
+
* exposes NO progress/heartbeat to drive a true idle-bound, so this is a GENEROUS wall-clock; the moment the SDK
|
|
61
|
+
* surfaces progress, these should move to a real liveness bound. OP-CLASS sized: cheap control RPCs
|
|
62
|
+
* (isRunning/kill/metadata) keep the short {@link DEFAULT_RPC_TIMEOUT_MS}; this only widens the transfer class.
|
|
63
|
+
*/
|
|
13
64
|
const DEFAULT_DATA_TIMEOUT_MS = 5 * 60_000;
|
|
65
|
+
/**
|
|
66
|
+
* design/103 background-shell bounds for E2B (TOB). A background process cannot outlive its sandbox, so the BG
|
|
67
|
+
* timeout ceiling is well under the per-sandbox max ({@link E2B_SANDBOX_MAX_MS}). `maxConcurrent` mirrors the TOC
|
|
68
|
+
* default (8) — anti fork-bomb, not a workload limit.
|
|
69
|
+
*/
|
|
14
70
|
const E2B_BG_MAX_CONCURRENT = numEnvOr("E2B_BG_MAX_CONCURRENT", 8, 1);
|
|
15
71
|
const E2B_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("E2B_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
|
|
16
72
|
const E2B_BG_MAX_TIMEOUT_SEC = numEnvOr("E2B_BG_MAX_TIMEOUT_SEC", 1800, E2B_BG_DEFAULT_TIMEOUT_SEC);
|
|
73
|
+
/** Pad (sec) added to the E2B command `timeoutMs` backstop so the control-plane hard wall flips status to `killed` FIRST. */
|
|
17
74
|
const E2B_BG_PROVIDER_BACKSTOP_PAD_SEC = 30;
|
|
18
75
|
const ok = (value) => ({ ok: true, value });
|
|
76
|
+
/**
|
|
77
|
+
* E2B-backed remote `ExecutionEnv`. **Lazy by default (v1b):** the VM is provisioned on the
|
|
78
|
+
* FIRST FileSystem/Shell/execStream call (auto `connect()`), so a task that never touches the hand pays zero
|
|
79
|
+
* cost. `connect()` may still be called explicitly (idempotent + concurrency-memoized) if the control plane
|
|
80
|
+
* wants to provision eagerly. `destroy()` on task end (the Runner calls it via `hasDestroy`); a never-used env's
|
|
81
|
+
* destroy is a no-op (nothing to kill, no bill). One container per task (design/48 §5 §7 Q7).
|
|
82
|
+
*/
|
|
19
83
|
export class RemoteContainerExecutionEnv {
|
|
84
|
+
/** Current working directory for relative paths (FileSystem contract; mutable, core may reassign). */
|
|
20
85
|
cwd;
|
|
21
86
|
cfg;
|
|
22
87
|
sandboxApi;
|
|
23
88
|
sbx;
|
|
89
|
+
/** 🔴 BL-31 keep-alive — the E2B sandbox lifetime is hard-capped (≈1h, {@link E2B_SANDBOX_MAX_MS}), but a long
|
|
90
|
+
* leader/repair run legitimately exceeds it. This interval RE-extends the sandbox (`setTimeout(timeoutMs)` resets
|
|
91
|
+
* the auto-kill window from now) while the env is held, so the sandbox tracks ACTUAL usage rather than dying
|
|
92
|
+
* mid-run at the cap (otherwise: a silent dead sandbox at hour 1). Cleared on destroy;
|
|
93
|
+
* a no-op while paused/destroyed. `unref`'d so it never holds the process open. */
|
|
24
94
|
keepAlive;
|
|
25
95
|
handle;
|
|
96
|
+
/** Terminal once set by {@link destroy} — never reset to false (no resurrection). Guards connect/ensureConnected
|
|
97
|
+
* and the post-await re-check in BOTH async-provision paths ({@link doConnect} and {@link doResumeVM}) against
|
|
98
|
+
* a destroy that races an in-flight provision/restore. */
|
|
26
99
|
destroyed = false;
|
|
100
|
+
/** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). */
|
|
27
101
|
connecting;
|
|
102
|
+
/** Count of exec/execStream calls in flight — `suspendVM` refuses while > 0 (council #4: never snapshot a running command). */
|
|
28
103
|
activeCommands = 0;
|
|
104
|
+
/** True between a `suspendVM` (VM paused, external connection dead) and the next `resumeVM` reconnect. While
|
|
105
|
+
* paused, `this.sbx` still points at the (now-dead) handle, so `resumeVM`'s idempotency guard must NOT treat
|
|
106
|
+
* it as live — it has to fall through and actually `Sandbox.connect()`. Without this, a suspend→resume on the
|
|
107
|
+
* SAME env instance skips the reconnect and every later fs/exec runs against the dead VM. */
|
|
29
108
|
paused = false;
|
|
30
109
|
constructor(config, deps = {}) {
|
|
31
110
|
this.cfg = {
|
|
32
111
|
...config,
|
|
33
112
|
mountPath: config.mountPath ?? DEFAULT_MOUNT_PATH,
|
|
113
|
+
// BL-31: clamp the sandbox lifetime to E2B's per-sandbox max (a 24h leaderTimeoutMs blows the cap → connect fails).
|
|
34
114
|
timeoutMs: Math.min(config.timeoutMs ?? DEFAULT_TIMEOUT_MS, E2B_SANDBOX_MAX_MS),
|
|
35
115
|
allowInternetAccess: config.allowInternetAccess ?? true,
|
|
36
116
|
rpcTimeoutMs: config.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
|
|
@@ -40,17 +120,30 @@ export class RemoteContainerExecutionEnv {
|
|
|
40
120
|
this.cwd = this.cfg.mountPath;
|
|
41
121
|
this.sandboxApi = deps.sandboxApi ?? Sandbox;
|
|
42
122
|
}
|
|
123
|
+
/** design/61 §9: E2B is a FULL RemoteExecutionEnv — an isolated, snapshot-suspendable microVM. (SSH/ADB
|
|
124
|
+
* adapters declare {isolation:false, suspendable:false} — real machines/devices, not snapshotable.) */
|
|
43
125
|
capabilities = { isolation: true, suspendable: true };
|
|
126
|
+
// ─────────────────────────────── RemoteExecutionEnv: lifecycle ───────────────────────────────
|
|
44
127
|
workspaceHandle() {
|
|
45
128
|
if (!this.handle) {
|
|
46
129
|
throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
|
|
47
130
|
}
|
|
48
131
|
return this.handle;
|
|
49
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Public host for a port inside the sandbox (E2B reverse proxy → `<port>-<id>.e2b.app`). The LSP sidecar
|
|
135
|
+
* (design/64 §13.1) uses this to reach the in-sandbox language-server WS bridge over `wss://`. Lazily
|
|
136
|
+
* provisions the VM (like exec/fs); `undefined` if it can't connect → the LSP manager degrades.
|
|
137
|
+
*/
|
|
50
138
|
async getHost(port) {
|
|
51
139
|
const conn = await this.ensureConnected();
|
|
52
140
|
return conn.ok ? conn.value.getHost(port) : undefined;
|
|
53
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Launch a long-running process as a TRUE E2B background command (resolves once launched, E2B keeps it alive)
|
|
144
|
+
* — for the LSP bridge (design/64 §13.1). NOT the same as `exec` with a shell `&`: E2B reaps a foreground
|
|
145
|
+
* command's process group on completion, which would kill an in-shell `&`/nohup child. Fire-and-forget.
|
|
146
|
+
*/
|
|
54
147
|
async startBackground(command) {
|
|
55
148
|
const conn = await this.ensureConnected();
|
|
56
149
|
if (conn.ok)
|
|
@@ -58,46 +151,57 @@ export class RemoteContainerExecutionEnv {
|
|
|
58
151
|
}
|
|
59
152
|
async connect(config) {
|
|
60
153
|
if (config?.snapshotId) {
|
|
154
|
+
// resume-from-snapshot lands via resumeVM (v1.5); connect() just delegates.
|
|
61
155
|
return this.resumeVM(config.snapshotId, config.abortSignal ? { abortSignal: config.abortSignal } : undefined);
|
|
62
156
|
}
|
|
63
157
|
if (this.destroyed)
|
|
64
158
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
159
|
+
// A paused (suspended) env still HOLDS this.sbx but the external connection is dead — returning it as live
|
|
160
|
+
// would let a late fs/exec hit the dead VM with a confusing low-level error. Fail clearly until resumeVM.
|
|
65
161
|
if (this.paused)
|
|
66
162
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
|
|
67
163
|
if (this.sbx)
|
|
68
|
-
return ok(this.workspaceHandle());
|
|
164
|
+
return ok(this.workspaceHandle()); // idempotent: already connected
|
|
69
165
|
if (this.connecting)
|
|
70
|
-
return this.connecting;
|
|
166
|
+
return this.connecting; // concurrent first-use shares one provision (lazy path)
|
|
71
167
|
this.connecting = this.doConnect(config);
|
|
72
168
|
const r = await this.connecting;
|
|
73
169
|
if (!r.ok)
|
|
74
|
-
this.connecting = undefined;
|
|
170
|
+
this.connecting = undefined; // failed connect → allow a later retry
|
|
75
171
|
return r;
|
|
76
172
|
}
|
|
77
173
|
async doConnect(config) {
|
|
78
174
|
try {
|
|
175
|
+
// ⚠️ This `await` yields — destroy() can run during it. A destroy that lands here sees sbx===undefined
|
|
176
|
+
// and returns without killing; if we then blindly assigned + reset `destroyed`, we'd RESURRECT a
|
|
177
|
+
// destroyed env and orphan a live billable VM (the Runner never calls destroy() twice). So after the
|
|
178
|
+
// await we re-check `destroyed` and kill-then-bail. Note: destroy is terminal — we never reset it to false.
|
|
79
179
|
const sbx = await this.rpc("sandbox.create", () => this.sandboxApi.create({
|
|
80
180
|
...(this.cfg.template ? { template: this.cfg.template } : {}),
|
|
81
181
|
apiKey: this.cfg.apiKey,
|
|
82
182
|
timeoutMs: this.cfg.timeoutMs,
|
|
83
183
|
allowInternetAccess: this.cfg.allowInternetAccess,
|
|
84
184
|
...(this.cfg.metadata ? { metadata: this.cfg.metadata } : {}),
|
|
185
|
+
// Out-of-band secret/config env (never logged) — sandbox-wide so every command + the LSP/bg process
|
|
186
|
+
// sees it without the value ever touching the model prompt or the tool command string.
|
|
85
187
|
...(this.cfg.sandboxEnv && Object.keys(this.cfg.sandboxEnv).length > 0 ? { envs: this.cfg.sandboxEnv } : {}),
|
|
86
188
|
}), config?.abortSignal, this.cfg.dataTimeoutMs);
|
|
87
189
|
if (this.destroyed) {
|
|
88
|
-
await sbx.kill().catch(() => { });
|
|
190
|
+
await sbx.kill().catch(() => { }); // raced with destroy() → kill the just-provisioned VM (no leak)
|
|
89
191
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
90
192
|
}
|
|
91
193
|
this.sbx = sbx;
|
|
92
194
|
this.paused = false;
|
|
93
|
-
this.startKeepAlive();
|
|
195
|
+
this.startKeepAlive(); // BL-31 — keep the sandbox alive past its ≤1h lifetime cap while the env is held.
|
|
94
196
|
this.cfg.logger?.info?.("remote_env_connect", { sandboxId: sbx.sandboxId });
|
|
95
197
|
this.handle = {
|
|
96
198
|
sandboxId: sbx.sandboxId,
|
|
97
199
|
provider: PROVIDER,
|
|
98
200
|
mountPath: this.cfg.mountPath,
|
|
99
|
-
sessionToken: sbx.sandboxId,
|
|
201
|
+
sessionToken: sbx.sandboxId, // a still-running E2B VM is re-attachable by its id (Sandbox.connect)
|
|
100
202
|
};
|
|
203
|
+
// design/48 §5 gap 7: secrets are injected at connect time. v1 defers secret *resolution* to the
|
|
204
|
+
// control plane (SecretRef carries only name+locator, no value) — recorded, wired in v1b/v1.5.
|
|
101
205
|
if (config?.secrets?.length) {
|
|
102
206
|
this.cfg.logger?.warn?.("remote-env: connect secrets are not injected in v1 (control-plane resolution deferred)", {
|
|
103
207
|
count: config.secrets.length,
|
|
@@ -111,6 +215,12 @@ export class RemoteContainerExecutionEnv {
|
|
|
111
215
|
return { ok: false, error: this.remoteError(e, "connect_failed") };
|
|
112
216
|
}
|
|
113
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* v1.5 (design/49): snapshot the workspace via E2B `pause()` (stores fs+memory+process; the snapshot id IS
|
|
220
|
+
* the sandbox id — a paused E2B sandbox is resumed by its id). Atomic contract (council): on failure the VM
|
|
221
|
+
* is left UNTOUCHED (still running) so core can fall back to onAsk. Refuses if a command is in flight
|
|
222
|
+
* (council #4) — never silently snapshot a running command. `abortSignal` bounds a hung pause (council BUG#2).
|
|
223
|
+
*/
|
|
114
224
|
async suspendVM(options) {
|
|
115
225
|
const sbx = this.sbx;
|
|
116
226
|
if (!sbx)
|
|
@@ -122,12 +232,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
122
232
|
}
|
|
123
233
|
try {
|
|
124
234
|
await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), options?.abortSignal, this.cfg.dataTimeoutMs);
|
|
235
|
+
// Same race as doConnect/doResumeVM: this `await` yielded, so destroy() may have landed. If destroy()
|
|
236
|
+
// killed a still-pausing sandbox its kill can be rejected, leaving a paused-but-unreferenced snapshot
|
|
237
|
+
// (billed storage until E2B's TTL); and we must NOT report a snapshotId for it — core would persist it
|
|
238
|
+
// and every later resume would fail. Best-effort kill (symmetric with doConnect/doResumeVM) + bail.
|
|
125
239
|
if (this.destroyed) {
|
|
240
|
+
// best-effort: if BOTH this and destroy()'s kill fail, a paused snapshot is orphaned (billed storage)
|
|
241
|
+
// until E2B's TTL — log it (destroy() logs its own kill failures too) so that is observable.
|
|
126
242
|
await sbx.kill().catch((e) => this.cfg.logger?.warn?.("remote-env: suspend-recheck kill failed (snapshot may orphan)", { error: errMsg(e) }));
|
|
127
243
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during suspend") };
|
|
128
244
|
}
|
|
129
|
-
this.paused = true;
|
|
130
|
-
this.connecting = undefined;
|
|
245
|
+
this.paused = true; // VM paused → external connection is dead; force resumeVM to actually reconnect
|
|
246
|
+
this.connecting = undefined; // drop the now-resolved prior provision promise so resumeVM doesn't return it instead of reconnecting
|
|
131
247
|
this.cfg.metrics?.inc("remote_env_suspend_total", { provider: PROVIDER, result: "ok" });
|
|
132
248
|
this.cfg.logger?.info?.("remote_env_suspend", { snapshotId: sbx.sandboxId });
|
|
133
249
|
return ok(sbx.sandboxId);
|
|
@@ -137,30 +253,48 @@ export class RemoteContainerExecutionEnv {
|
|
|
137
253
|
return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "unknown") };
|
|
138
254
|
}
|
|
139
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* v1.5: restore from a snapshot via E2B `Sandbox.connect(snapshotId)` (auto-resumes a paused sandbox).
|
|
258
|
+
* Idempotent: a no-op if already on that sandbox. Concurrent resume/connect for the same snapshot share ONE
|
|
259
|
+
* provision (memoized via `connecting`, like {@link connect}) so a racing call never double-creates a billable
|
|
260
|
+
* VM (council #3). `abortSignal` bounds a hung restore.
|
|
261
|
+
*/
|
|
140
262
|
async resumeVM(snapshotId, options) {
|
|
141
263
|
if (this.destroyed)
|
|
142
264
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
|
|
143
265
|
if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
|
|
144
|
-
return ok(this.workspaceHandle());
|
|
266
|
+
return ok(this.workspaceHandle()); // already resumed + live (idempotent); a paused sbx must reconnect
|
|
145
267
|
if (this.connecting)
|
|
146
|
-
return this.connecting;
|
|
268
|
+
return this.connecting; // concurrent resume/connect shares one provision (council #3)
|
|
147
269
|
this.connecting = this.doResumeVM(snapshotId, options);
|
|
148
270
|
const r = await this.connecting;
|
|
149
271
|
if (!r.ok)
|
|
150
|
-
this.connecting = undefined;
|
|
272
|
+
this.connecting = undefined; // failed resume → allow a later retry
|
|
151
273
|
return r;
|
|
152
274
|
}
|
|
153
275
|
async doResumeVM(snapshotId, options) {
|
|
154
276
|
try {
|
|
277
|
+
// Carry the SAME apiKey + lifetime as doConnect. `SandboxConnectOpts.timeoutMs` defaults to 5 minutes;
|
|
278
|
+
// omitting it would reset a resumed VM's lifetime to 5min regardless of cfg.timeoutMs, so a long task
|
|
279
|
+
// resumed after a durable-approval ask would be reaped by E2B mid-run (snapshot already consumed). The
|
|
280
|
+
// apiKey is explicit too (don't depend on the SDK's E2B_API_KEY env fallback once keys move to the
|
|
281
|
+
// control plane). Wrapped in `rpc` so the connect also gets the data-transfer timeout/abort.
|
|
155
282
|
const sbx = await this.rpc("sandbox.connect", (signal) => this.sandboxApi.connect(snapshotId, { apiKey: this.cfg.apiKey, timeoutMs: this.cfg.timeoutMs, signal }), options?.abortSignal, this.cfg.dataTimeoutMs);
|
|
283
|
+
// Same race as doConnect: this `await` yields → destroy() can land during it. Re-check + kill-then-bail
|
|
284
|
+
// so we never assign a live VM to a destroyed env (which would orphan a billable sandbox — destroy() is
|
|
285
|
+
// terminal and won't be called again). (council #1)
|
|
156
286
|
if (this.destroyed) {
|
|
157
287
|
await sbx.kill().catch(() => { });
|
|
158
288
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
|
|
159
289
|
}
|
|
160
290
|
this.sbx = sbx;
|
|
161
291
|
this.paused = false;
|
|
162
|
-
this.startKeepAlive();
|
|
292
|
+
this.startKeepAlive(); // BL-31 — re-arm the keep-alive on a resumed sandbox too.
|
|
163
293
|
this.cfg.logger?.info?.("remote_env_resume", { requested: snapshotId, connected: sbx.sandboxId });
|
|
294
|
+
// [1467]①(core 1.360 VmLifecycleOptions.priorHandle):恢复根=checkpoint 的 mountPath——E2B resume
|
|
295
|
+
// 恢复的是整个 VM 文件系统,旧根里的产物原位在;当前配置值只该管新建沙箱。exec/fs/mktemp 三腿同源
|
|
296
|
+
// 消费 this.cwd,一并重基;handle 回报的就是真生效根(诚实契约:绝不回显未兑现值)。缺 priorHandle
|
|
297
|
+
// (旧 core/旧 checkpoint)= 落当前配置值——此窗内改 E2B_MOUNT_PATH 仍需先 drain 挂起任务。
|
|
164
298
|
const restoredRoot = options?.priorHandle?.mountPath ?? this.cfg.mountPath;
|
|
165
299
|
this.cwd = restoredRoot;
|
|
166
300
|
this.handle = { sandboxId: sbx.sandboxId, provider: PROVIDER, mountPath: restoredRoot, snapshotId, sessionToken: sbx.sandboxId };
|
|
@@ -172,9 +306,17 @@ export class RemoteContainerExecutionEnv {
|
|
|
172
306
|
return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "connect_failed") };
|
|
173
307
|
}
|
|
174
308
|
}
|
|
309
|
+
/** Cross-replica re-attach to a still-running VM = v2 (design/49 council #15: v1.5 always snapshots→resumeVM). */
|
|
175
310
|
async reconnect(_sessionToken) {
|
|
176
311
|
return { ok: false, error: new RemoteExecutionError("unknown", "reconnect is deferred to v2 (v1.5 always snapshots → use resumeVM)") };
|
|
177
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* v1.5: re-establish consistency after resumeVM. The resumed VM is restored to its paused state (fs+memory+
|
|
315
|
+
* process); this verifies it is reachable. Per-command secrets are re-applied automatically (this adapter
|
|
316
|
+
* injects envs per command, not persistently). The VM-internal agent rebuilds its own external connections
|
|
317
|
+
* (git/API don't survive pause). 🔴 At-rest snapshot encryption is the provider's here (E2B managed) — a
|
|
318
|
+
* self-hosted E2B (v2) MUST guarantee it before any secret injection (remote-env.ts §5 red line).
|
|
319
|
+
*/
|
|
178
320
|
async postResumeInit() {
|
|
179
321
|
const sbx = this.sbx;
|
|
180
322
|
if (!sbx)
|
|
@@ -189,12 +331,15 @@ export class RemoteContainerExecutionEnv {
|
|
|
189
331
|
return { ok: false, error: this.remoteError(e, "post_resume_failed") };
|
|
190
332
|
}
|
|
191
333
|
}
|
|
334
|
+
/** BL-31 — (re)start the periodic sandbox-lifetime extension. `setTimeout(timeoutMs)` resets the auto-kill window
|
|
335
|
+
* from now; we re-arm at ~80% of the (clamped, ≤1h) window so a long-held sandbox never expires mid-run. A no-op
|
|
336
|
+
* when paused/destroyed; `unref`'d so it never keeps the process alive; cleared on destroy. */
|
|
192
337
|
startKeepAlive() {
|
|
193
338
|
this.stopKeepAlive();
|
|
194
339
|
const everyMs = Math.max(30_000, Math.floor(this.cfg.timeoutMs * 0.8));
|
|
195
340
|
this.keepAlive = setInterval(() => {
|
|
196
341
|
if (this.destroyed || this.paused || !this.sbx)
|
|
197
|
-
return;
|
|
342
|
+
return; // paused/dead → the resume path re-arms it
|
|
198
343
|
void this.sbx.setTimeout(this.cfg.timeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_keepalive_failed", { error: errMsg(e) }));
|
|
199
344
|
}, everyMs);
|
|
200
345
|
this.keepAlive.unref?.();
|
|
@@ -207,37 +352,47 @@ export class RemoteContainerExecutionEnv {
|
|
|
207
352
|
}
|
|
208
353
|
async destroy() {
|
|
209
354
|
if (this.destroyed)
|
|
210
|
-
return;
|
|
355
|
+
return; // idempotent — safe double-call (Runner end + reaper)
|
|
211
356
|
this.destroyed = true;
|
|
212
357
|
this.connecting = undefined;
|
|
213
|
-
this.stopKeepAlive();
|
|
214
|
-
await this.disposeBackgroundShells();
|
|
358
|
+
this.stopKeepAlive(); // BL-31 — stop extending the sandbox lifetime; it's being killed below.
|
|
359
|
+
await this.disposeBackgroundShells(); // design/103 §3.7 backstop — clears BG timers (the VM kill below reaps the jobs)
|
|
215
360
|
const sbx = this.sbx;
|
|
216
361
|
this.sbx = undefined;
|
|
217
362
|
if (!sbx)
|
|
218
|
-
return;
|
|
363
|
+
return; // never connected (lazy task that didn't touch the hand) → nothing to kill, zero bill
|
|
219
364
|
const startedAt = Date.now();
|
|
220
365
|
try {
|
|
221
366
|
await this.rpc("sandbox.kill", () => sbx.kill());
|
|
222
367
|
this.cfg.metrics?.inc("remote_env_destroy_total", { provider: PROVIDER, result: "ok" });
|
|
368
|
+
// Symmetric with remote_env_connect: a clean teardown line carrying the same sandboxId
|
|
369
|
+
// gives the cleanup leg a log-only proof — no need to cross-check the provider's running-sandbox API.
|
|
223
370
|
this.cfg.logger?.info?.("remote_env_destroy", { sandboxId: sbx.sandboxId, ms: Date.now() - startedAt });
|
|
224
371
|
}
|
|
225
372
|
catch (e) {
|
|
373
|
+
// best-effort, must never throw (cleanup contract)
|
|
226
374
|
this.cfg.metrics?.inc("remote_env_destroy_total", { provider: PROVIDER, result: "error" });
|
|
227
375
|
this.cfg.logger?.warn?.("remote_env_destroy_failed", { sandboxId: sbx.sandboxId, ms: Date.now() - startedAt, error: errMsg(e) });
|
|
228
376
|
}
|
|
229
377
|
}
|
|
378
|
+
// FileSystem + Shell both declare cleanup(); both map to destroy() (release all VM resources).
|
|
230
379
|
async cleanup() {
|
|
231
380
|
await this.destroy();
|
|
232
381
|
}
|
|
382
|
+
// ─────────────────────────────── Shell: buffered exec ───────────────────────────────
|
|
233
383
|
async exec(command, options) {
|
|
384
|
+
// Already cancelled → don't lazily provision a billable VM just to immediately abort it.
|
|
234
385
|
if (options?.abortSignal?.aborted)
|
|
235
386
|
return { ok: false, error: new ExecutionError("aborted", "exec aborted before start") };
|
|
236
|
-
const conn = await this.ensureConnected();
|
|
387
|
+
const conn = await this.ensureConnected(); // lazy: first use provisions the VM
|
|
237
388
|
if (!conn.ok)
|
|
238
389
|
return { ok: false, error: new ExecutionError("shell_unavailable", `remote connect failed: ${conn.error.message}`, conn.error) };
|
|
239
390
|
const sbx = conn.value;
|
|
240
|
-
this.activeCommands++;
|
|
391
|
+
this.activeCommands++; // in-flight: suspendVM refuses while > 0 (council #4)
|
|
392
|
+
// design/116 detach (core 1.207, 1.82): the Bash tool threads a `detachSignal` when the operator can mid-flight
|
|
393
|
+
// ctrl+b. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally. When present, tee all output
|
|
394
|
+
// into a rolling-tail pair shaped EXACTLY like the bg driver's per-shell state (E2bBgStream) — adoption is then
|
|
395
|
+
// a zero-copy handover of {handle, stdout, stderr} into the SAME state the launch path builds.
|
|
241
396
|
const detachSignal = options?.detachSignal;
|
|
242
397
|
const tee = detachSignal
|
|
243
398
|
? {
|
|
@@ -251,13 +406,23 @@ export class RemoteContainerExecutionEnv {
|
|
|
251
406
|
s.tail.push(b);
|
|
252
407
|
s.total += b.length;
|
|
253
408
|
};
|
|
254
|
-
|
|
409
|
+
// Run in the background and keep the handle so an abort can actually KILL the remote process. A plain
|
|
410
|
+
// `commands.run` only aborts the local request — the VM process keeps running (and billing) until the
|
|
411
|
+
// sandbox times out. The handle still buffers stdout/stderr; wait() returns the full CommandResult.
|
|
412
|
+
let lastProgress = Date.now(); // bumped by stdout/stderr → drives the wait()-phase liveness (waitWithLiveness)
|
|
255
413
|
let handle;
|
|
256
414
|
try {
|
|
415
|
+
// 🔴 Bound the command-CREATION RPC with the liveness timer — a provider hang here (before the
|
|
416
|
+
// command even starts) used to wait the whole sandbox lifetime. The wait() phase below is then
|
|
417
|
+
// bounded by waitWithLiveness (idle-bound), so BOTH phases of a hang are covered.
|
|
257
418
|
handle = await this.rpc("commands.run", () => sbx.commands.run(command, {
|
|
258
419
|
background: true,
|
|
259
420
|
cwd: this.resolve(options?.cwd ?? this.cwd),
|
|
260
421
|
...(options?.env ? { envs: options.env } : {}),
|
|
422
|
+
// Detach-capable exec: the provider command timeout is fixed at creation, so it must not undercut a
|
|
423
|
+
// LATER adoption's BG wall — lift it to the BG ceiling (+backstop pad, same discipline as the bg
|
|
424
|
+
// launch) and enforce the FOREGROUND wall with the control-plane `wallMs` in waitWithLiveness below
|
|
425
|
+
// (disarmed on adoption). Without a detachSignal the provider timeout stays the wall, as before.
|
|
261
426
|
timeoutMs: tee
|
|
262
427
|
? Math.max(this.commandTimeoutMs(options?.timeout), (E2B_BG_MAX_TIMEOUT_SEC + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000)
|
|
263
428
|
: this.commandTimeoutMs(options?.timeout),
|
|
@@ -283,6 +448,8 @@ export class RemoteContainerExecutionEnv {
|
|
|
283
448
|
this.activeCommands--;
|
|
284
449
|
return { ok: false, error: this.execError(e, options?.abortSignal) };
|
|
285
450
|
}
|
|
451
|
+
// Post-adoption the abort belongs to the (already-settled) foreground tool call — it must NOT kill the
|
|
452
|
+
// adopted background job (core aborts the per-call signal right after the early tool result).
|
|
286
453
|
const onAbort = () => { if (!detached)
|
|
287
454
|
void handle.kill().catch(() => { }); };
|
|
288
455
|
if (options?.abortSignal) {
|
|
@@ -291,8 +458,10 @@ export class RemoteContainerExecutionEnv {
|
|
|
291
458
|
else
|
|
292
459
|
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
293
460
|
}
|
|
461
|
+
// 🔴 Consume `handle.wait()` exactly ONCE (the SDK processes the command's event stream in it): the foreground
|
|
462
|
+
// race and the post-adoption terminal notification both hang off this single promise.
|
|
294
463
|
const waitP = handle.wait();
|
|
295
|
-
let fgResolved = false;
|
|
464
|
+
let fgResolved = false; // microtask-latch: a detach landing after the command finished must NOT adopt (double-represent)
|
|
296
465
|
const markResolved = () => { fgResolved = true; };
|
|
297
466
|
waitP.then(markResolved, markResolved);
|
|
298
467
|
let fgSettled = false;
|
|
@@ -301,8 +470,19 @@ export class RemoteContainerExecutionEnv {
|
|
|
301
470
|
const detachedP = detachSignal && tee
|
|
302
471
|
? new Promise((res) => {
|
|
303
472
|
onDetach = () => {
|
|
473
|
+
// 🔴 review MED: E2B's onAbort only fires an async `handle.kill()` (no synchronous settle flag), unlike
|
|
474
|
+
// host/k8s whose abort synchronously settles (finish→settled) and whose onDetach guards on it. Without
|
|
475
|
+
// `abortSignal?.aborted` here, an abort in flight (kill RPC not yet completed, so `fgResolved` unset)
|
|
476
|
+
// followed by a detach would ADOPT a process that is already being killed — the aborted call would return
|
|
477
|
+
// detached success and hand the operator a corpse shellId. Align E2B with the settled-blocks-detach rule.
|
|
304
478
|
if (detached || fgSettled || fgResolved || options?.abortSignal?.aborted)
|
|
305
479
|
return;
|
|
480
|
+
// SYNC adopt (manager contract): the tee buffers already hold every byte from process start (the
|
|
481
|
+
// onStdout/onStderr closures keep feeding them — no listener re-registration exists on an E2B
|
|
482
|
+
// CommandHandle, which is exactly why the tee is created up-front). Terminal rides the shared waitP.
|
|
483
|
+
// 🔴 builder contract (Fable-5 review #5): everything inside is NON-THROWING (.then/.catch on an
|
|
484
|
+
// existing promise + object construction). adoptSync's catch does NOT undo side effects — future
|
|
485
|
+
// logic that can throw must go before the first side effect.
|
|
306
486
|
const adopted = this.bgManager.adoptSync((ctx) => {
|
|
307
487
|
waitP
|
|
308
488
|
.then((r) => ctx.onTerminal(false, r.exitCode))
|
|
@@ -315,11 +495,13 @@ export class RemoteContainerExecutionEnv {
|
|
|
315
495
|
return { handle, stdout: tee.stdout, stderr: tee.stderr };
|
|
316
496
|
});
|
|
317
497
|
if (!adopted)
|
|
318
|
-
return;
|
|
498
|
+
return; // refused (limit) → the exec keeps running in the foreground
|
|
319
499
|
adoptedShellId = adopted.shellId;
|
|
320
|
-
detached = true;
|
|
500
|
+
detached = true; // stops caller-forwarding + disarms the fg liveness/wall kills
|
|
321
501
|
res("detached");
|
|
322
502
|
};
|
|
503
|
+
// core's ToolDetachHub is create-then-abort: a detach that landed BEFORE this exec started arrives as an
|
|
504
|
+
// ALREADY-aborted signal → adopt immediately (everything onDetach touches is initialized above).
|
|
323
505
|
if (detachSignal.aborted)
|
|
324
506
|
onDetach();
|
|
325
507
|
else
|
|
@@ -330,6 +512,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
330
512
|
const fg = this.waitWithLiveness(handle, () => lastProgress, {
|
|
331
513
|
waitP,
|
|
332
514
|
disarmed: () => detached,
|
|
515
|
+
// Foreground wall, control-plane enforced on the detach-capable path (provider timeout was lifted above).
|
|
333
516
|
...(tee ? { wallMs: this.commandTimeoutMs(options?.timeout) } : {}),
|
|
334
517
|
});
|
|
335
518
|
const winner = await (detachedP ? Promise.race([fg, detachedP]) : fg);
|
|
@@ -343,6 +526,11 @@ export class RemoteContainerExecutionEnv {
|
|
|
343
526
|
detached: { shellId: adoptedShellId },
|
|
344
527
|
});
|
|
345
528
|
}
|
|
529
|
+
// 夜测 N2(2026-07-09,MED):an aborted exec's Result shape was NON-DETERMINISTIC — the abort path only
|
|
530
|
+
// fires an async handle.kill(), after which wait() may settle as a plain result OR a CommandExitError
|
|
531
|
+
// (non-zero exit from the kill), both previously returned ok:true; only an rpc-layer failure surfaced
|
|
532
|
+
// "aborted". Normalize: an aborted, non-detached exec ALWAYS returns {ok:false, error:"aborted"} (the
|
|
533
|
+
// remote process IS terminated either way — this only stabilizes the caller's Result contract).
|
|
346
534
|
if (options?.abortSignal?.aborted && !detached) {
|
|
347
535
|
return { ok: false, error: new ExecutionError("aborted", "exec aborted (remote process killed)") };
|
|
348
536
|
}
|
|
@@ -350,8 +538,9 @@ export class RemoteContainerExecutionEnv {
|
|
|
350
538
|
}
|
|
351
539
|
catch (e) {
|
|
352
540
|
if (options?.abortSignal?.aborted && !detached) {
|
|
353
|
-
return { ok: false, error: this.execError(e, options.abortSignal) };
|
|
541
|
+
return { ok: false, error: this.execError(e, options.abortSignal) }; // N2: classified "aborted" (see above)
|
|
354
542
|
}
|
|
543
|
+
// E2B throws CommandExitError on non-zero exit, but ExecutionEnv treats exitCode as a normal value.
|
|
355
544
|
if (e instanceof CommandExitError) {
|
|
356
545
|
return ok({ stdout: e.stdout, stderr: e.stderr, exitCode: e.exitCode });
|
|
357
546
|
}
|
|
@@ -365,11 +554,13 @@ export class RemoteContainerExecutionEnv {
|
|
|
365
554
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
366
555
|
}
|
|
367
556
|
}
|
|
557
|
+
// ─────────────────────────────── RemoteExecutionEnv: streaming exec ───────────────────────────────
|
|
368
558
|
async *execStream(command, options) {
|
|
369
|
-
const conn = await this.ensureConnected();
|
|
559
|
+
const conn = await this.ensureConnected(); // lazy: first use provisions the VM
|
|
370
560
|
if (!conn.ok)
|
|
371
561
|
throw conn.error;
|
|
372
562
|
const sbx = conn.value;
|
|
563
|
+
// Producer/consumer queue: E2B delivers output via onStdout/onStderr callbacks; we pull as an iterator.
|
|
373
564
|
const queue = [];
|
|
374
565
|
let finished = false;
|
|
375
566
|
let failure;
|
|
@@ -384,11 +575,15 @@ export class RemoteContainerExecutionEnv {
|
|
|
384
575
|
const cap = options?.maxOutputBytes;
|
|
385
576
|
const push = (type, data) => {
|
|
386
577
|
if (finished)
|
|
387
|
-
return;
|
|
578
|
+
return; // terminal-latch: once finished, drop any late E2B callback (never enqueue into a dead stream)
|
|
388
579
|
bytes += Buffer.byteLength(data, "utf8");
|
|
389
580
|
queue.push({ type, data });
|
|
390
581
|
signalReady();
|
|
391
582
|
};
|
|
583
|
+
// Increment BEFORE awaiting commands.run (mirrors exec): `commands.run` is a round-trip that may start the
|
|
584
|
+
// command on the VM before it resolves, so suspendVM must already see activeCommands > 0 to refuse — else a
|
|
585
|
+
// suspend racing the launch could snapshot a running command (council #4 invariant). Decrement on launch
|
|
586
|
+
// failure here; the normal path decrements in the outer finally.
|
|
392
587
|
this.activeCommands++;
|
|
393
588
|
let handle;
|
|
394
589
|
try {
|
|
@@ -402,9 +597,14 @@ export class RemoteContainerExecutionEnv {
|
|
|
402
597
|
}), options?.signal, this.cfg.livenessMs);
|
|
403
598
|
}
|
|
404
599
|
catch (e) {
|
|
600
|
+
// failed before any exit chunk → throw (streaming analogue of never-throw Result)
|
|
405
601
|
this.activeCommands--;
|
|
406
602
|
throw this.remoteError(e, "connect_failed");
|
|
407
603
|
}
|
|
604
|
+
// Completion: E2B throws CommandExitError on non-zero exit — that is a NORMAL finish, not a stream failure.
|
|
605
|
+
// `finished` is a first-writer-wins latch: whichever of {wait-resolve, wait-reject, abort} fires first sets
|
|
606
|
+
// the terminal state; the rest no-op. Without it, an abort (or a late wait settlement) firing AFTER a clean
|
|
607
|
+
// completion — but before the consumer pulls the final chunk — would clobber a successful exit into a throw.
|
|
408
608
|
handle
|
|
409
609
|
.wait()
|
|
410
610
|
.then((r) => {
|
|
@@ -440,13 +640,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
440
640
|
else
|
|
441
641
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
442
642
|
}
|
|
643
|
+
// Drain via a read-index, not Array.shift() — shift() is O(n) per call → O(n²) on large output (a
|
|
644
|
+
// `npm ci`/build streaming millions of lines). Compacts once fully caught up. Throws (after killing the
|
|
645
|
+
// command) on maxOutputBytes. Single helper so the main loop and the post-completion flush stay in lockstep.
|
|
443
646
|
let head = 0;
|
|
444
647
|
const drainQueued = function* () {
|
|
445
648
|
while (head < queue.length) {
|
|
446
649
|
const chunk = queue[head++];
|
|
447
650
|
yield chunk;
|
|
651
|
+
// Soft cap: checked AFTER yield, so the chunk that crosses `cap` is still delivered (bounded by one
|
|
652
|
+
// E2B chunk). It's flood-protection (stop a `find /` drowning the control plane), not a byte-exact limit.
|
|
448
653
|
if (cap != null && bytes > cap) {
|
|
449
|
-
finished = true;
|
|
654
|
+
finished = true; // latch before kill (symmetry with onAbort): blocks late callbacks + the finally's re-kill
|
|
450
655
|
void handle.kill().catch(() => { });
|
|
451
656
|
throw new RemoteExecutionError("unknown", `execStream output exceeded maxOutputBytes=${cap}`);
|
|
452
657
|
}
|
|
@@ -459,9 +664,15 @@ export class RemoteContainerExecutionEnv {
|
|
|
459
664
|
yield* drainQueued();
|
|
460
665
|
if (finished)
|
|
461
666
|
break;
|
|
667
|
+
// Idle read-timeout (design/48 §5, E2B #1128): a streaming call has no read timeout, so an
|
|
668
|
+
// unreachable sandbox would hang forever. Bound the wait for the NEXT chunk and kill on expiry.
|
|
462
669
|
const next = new Promise((resolve) => {
|
|
463
670
|
wake = resolve;
|
|
464
671
|
});
|
|
672
|
+
// 🔴 Liveness is DEFAULT-ON: a missing readTimeoutMs MUST NOT mean "wait forever / until the env
|
|
673
|
+
// dies" — fall back to cfg.livenessMs. (The command-CREATION RPC is bounded separately by rpc() above, so
|
|
674
|
+
// the two together cover both phases of a provider hang.) Only kills a ZERO-PROGRESS command — a slow but
|
|
675
|
+
// streaming `npm ci` keeps resetting the timer via push()→signalReady().
|
|
465
676
|
const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
|
|
466
677
|
let timer;
|
|
467
678
|
const idle = new Promise((_, reject) => {
|
|
@@ -471,7 +682,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
471
682
|
await Promise.race([next, idle]);
|
|
472
683
|
}
|
|
473
684
|
catch (e) {
|
|
474
|
-
finished = true;
|
|
685
|
+
finished = true; // latch before kill (symmetry with onAbort): blocks late callbacks + the finally's re-kill
|
|
475
686
|
void handle.kill().catch(() => { });
|
|
476
687
|
this.cfg.metrics?.inc("remote_env_exec_stream_read_timeout_total", { provider: PROVIDER });
|
|
477
688
|
throw e instanceof RemoteExecutionError ? e : this.remoteError(e);
|
|
@@ -481,12 +692,16 @@ export class RemoteContainerExecutionEnv {
|
|
|
481
692
|
clearTimeout(timer);
|
|
482
693
|
}
|
|
483
694
|
}
|
|
484
|
-
yield* drainQueued();
|
|
695
|
+
yield* drainQueued(); // flush any chunks delivered alongside completion (same cap/kill semantics)
|
|
485
696
|
if (failure)
|
|
486
697
|
throw failure;
|
|
487
698
|
yield { type: "exit", exitCode };
|
|
488
699
|
}
|
|
489
700
|
finally {
|
|
701
|
+
// If the consumer abandons the iterator early (`break`/upstream cancel without an abort signal), the
|
|
702
|
+
// generator runs only this finally — the remote command would otherwise keep running (and billing) for
|
|
703
|
+
// its whole timeout, AND activeCommands would drop to 0 with a live command, letting suspendVM snapshot a
|
|
704
|
+
// running command. Kill on any non-finished exit so the council #4 invariant holds on every path.
|
|
490
705
|
if (!finished) {
|
|
491
706
|
finished = true;
|
|
492
707
|
void handle.kill().catch(() => { });
|
|
@@ -495,11 +710,34 @@ export class RemoteContainerExecutionEnv {
|
|
|
495
710
|
options?.signal?.removeEventListener("abort", onAbort);
|
|
496
711
|
}
|
|
497
712
|
}
|
|
713
|
+
// ─────────────────────────── design/103 background shell (TOB, path ②) ───────────────────────────
|
|
714
|
+
//
|
|
715
|
+
// E2B takes path ② (design/103 §5.2): a resident control-plane reader. `commands.run({background:true})` returns
|
|
716
|
+
// a `CommandHandle` immediately whose `onStdout`/`onStderr` callbacks keep firing as output arrives; we tee them
|
|
717
|
+
// into a per-shell rolling-tail buffer and serve `pollBackground` from that buffer's cursor. Consequence (design
|
|
718
|
+
// §5.2): the buffer is REPLICA-AFFINE — it lives in this env instance's heap and does NOT survive a reconnect to
|
|
719
|
+
// another replica. That is fine: background shells never cross a durable suspend (the Runner disposes them before
|
|
720
|
+
// `suspendVM`, design/103 §3.7), and a reconnected replica's empty registry yields `not_found` (VM destroy reaps
|
|
721
|
+
// the orphaned in-sandbox job). Background jobs deliberately do NOT touch `activeCommands` — they are long-lived
|
|
722
|
+
// and would otherwise wedge `suspendVM` forever; the dispose-before-suspend ordering (core saga) is what keeps a
|
|
723
|
+
// running background job from being snapshotted.
|
|
724
|
+
//
|
|
725
|
+
// 🔴 KILL SCOPE (honest, live-verified limitation): `handle.kill()` reliably SIGKILLs the MAIN process the model
|
|
726
|
+
// started — which is the model's KillShell intent (the dev server stops, its port frees). It does NOT group-kill
|
|
727
|
+
// the subtree, and E2B offers no SAFE way to: the per-command process group is SHARED with envd (killing it kills
|
|
728
|
+
// the sandbox), a ppid tree-walk doesn't reach E2B's reparented/disowned descendants, and `/proc/<pid>/environ`
|
|
729
|
+
// is ptrace-hardened (an env-marker scan reads nothing). All probed live. So detached grandchildren are reaped by
|
|
730
|
+
// the sandbox VM teardown (`destroy()`/`sbx.kill()`), which ALWAYS runs at task end — the reliable reaper (mirrors
|
|
731
|
+
// TOC's crash-honesty: we don't fake a group-kill the provider can't safely do). Practical impact is bounded:
|
|
732
|
+
// KillShell stops the server; on dispose-before-suspend the main processes die and any survivor is frozen (paused
|
|
733
|
+
// VM = no CPU) then reaped at the eventual destroy. Relayed to core for the design ledger.
|
|
498
734
|
backgroundCapabilities = {
|
|
499
735
|
supported: true,
|
|
500
736
|
maxConcurrent: E2B_BG_MAX_CONCURRENT,
|
|
501
737
|
defaultBgTimeoutSec: E2B_BG_DEFAULT_TIMEOUT_SEC,
|
|
502
738
|
maxBgTimeoutSec: E2B_BG_MAX_TIMEOUT_SEC,
|
|
739
|
+
// core 1.207 design/116 detach (1.82): a running foreground exec can be ADOPTED as a background shell
|
|
740
|
+
// (mid-flight ctrl+b) — the exec's CommandHandle + tee buffers hand over into the launch-shaped state.
|
|
503
741
|
supportsDetach: true,
|
|
504
742
|
};
|
|
505
743
|
_bgManager;
|
|
@@ -510,16 +748,21 @@ export class RemoteContainerExecutionEnv {
|
|
|
510
748
|
const self = this;
|
|
511
749
|
return {
|
|
512
750
|
async launch(command, options, ctx) {
|
|
513
|
-
const conn = await self.ensureConnected();
|
|
751
|
+
const conn = await self.ensureConnected(); // lazy: first use provisions the VM
|
|
514
752
|
if (!conn.ok)
|
|
515
|
-
throw new Error(`remote connect failed: ${conn.error.message}`);
|
|
753
|
+
throw new Error(`remote connect failed: ${conn.error.message}`); // → manager wraps as spawn_failed
|
|
516
754
|
const sbx = conn.value;
|
|
517
755
|
const stdout = { tail: new RollingTailBuffer(), total: 0, cursor: 0 };
|
|
518
756
|
const stderr = { tail: new RollingTailBuffer(), total: 0, cursor: 0 };
|
|
757
|
+
// 🔴 secret-scrub red line (design/103 §3.1): env is passed EXACTLY as `exec`/`execStream` do
|
|
758
|
+
// (`{ envs: options.env }`) — the host process.env is never inherited; the sandbox base env is the
|
|
759
|
+
// operator-trusted `sandboxEnv` set at create. A background child therefore sees the SAME scrubbed env.
|
|
519
760
|
const handle = await self.rpc("commands.run.bg", () => sbx.commands.run(command, {
|
|
520
761
|
background: true,
|
|
521
762
|
cwd: self.resolve(options?.cwd ?? self.cwd),
|
|
522
763
|
...(options?.env ? { envs: options.env } : {}),
|
|
764
|
+
// Provider-level backstop ONLY: padded past the control-plane hard wall so the manager's timer flips
|
|
765
|
+
// status to `killed` first; this reaps the in-sandbox job if the control plane dies (design/103 §3.6).
|
|
523
766
|
timeoutMs: (ctx.bgTimeoutSec + E2B_BG_PROVIDER_BACKSTOP_PAD_SEC) * 1000,
|
|
524
767
|
onStdout: (d) => {
|
|
525
768
|
const b = Buffer.from(d, "utf8");
|
|
@@ -532,6 +775,8 @@ export class RemoteContainerExecutionEnv {
|
|
|
532
775
|
stderr.total += b.length;
|
|
533
776
|
},
|
|
534
777
|
}), undefined, self.cfg.livenessMs);
|
|
778
|
+
// Push-model terminal (design/103): wait() resolves on clean exit, rejects with CommandExitError on non-zero
|
|
779
|
+
// (a NORMAL finish → exited), or any other error → failed. `.catch` is mandatory (no unhandled rejection).
|
|
535
780
|
handle
|
|
536
781
|
.wait()
|
|
537
782
|
.then((r) => ctx.onTerminal(false, r.exitCode))
|
|
@@ -545,10 +790,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
545
790
|
},
|
|
546
791
|
async read(state) {
|
|
547
792
|
const slice = (s) => {
|
|
793
|
+
// Cursor over ABSOLUTE byte offsets; the tail retains [droppedBytes, total). Return [cursor, total).
|
|
548
794
|
const { text, droppedBytes } = s.tail.result();
|
|
549
795
|
const droppedBeforeCursor = Math.max(0, droppedBytes - s.cursor);
|
|
550
796
|
const startByte = Math.max(s.cursor, droppedBytes);
|
|
551
797
|
const startInTail = startByte - droppedBytes;
|
|
798
|
+
// 🔴 re-encode delta (Fable-5 review #2): `startInTail` is an ORIGINAL-byte offset, but `text` is a
|
|
799
|
+
// decode→re-encode round trip. The tail's content is always valid UTF-8 (fed from SDK strings) EXCEPT
|
|
800
|
+
// its very head when an evict partially cut a multibyte codepoint — each stranded byte decodes to one
|
|
801
|
+
// U+FFFD (3 bytes re-encoded), so the re-encoded buffer is `delta` bytes longer than the original tail
|
|
802
|
+
// and the difference sits entirely BEFORE any valid cursor position (the cursor always lands on an SDK
|
|
803
|
+
// string boundary, past the stranded head). Shift the slice start by delta; without it the increment
|
|
804
|
+
// re-served up to 2 already-returned bytes per stranded byte (byte-drifted output).
|
|
552
805
|
const reencoded = Buffer.from(text, "utf8");
|
|
553
806
|
const delta = reencoded.length - (s.total - droppedBytes);
|
|
554
807
|
const inc = startInTail <= 0 ? text : reencoded.subarray(startInTail + delta).toString("utf8");
|
|
@@ -565,10 +818,10 @@ export class RemoteContainerExecutionEnv {
|
|
|
565
818
|
};
|
|
566
819
|
},
|
|
567
820
|
async kill(state) {
|
|
568
|
-
await state.handle.kill().catch(() => { });
|
|
821
|
+
await state.handle.kill().catch(() => { }); // idempotent: no-op if already settled
|
|
569
822
|
},
|
|
570
823
|
async disposeOne(state) {
|
|
571
|
-
await state.handle.kill().catch(() => { });
|
|
824
|
+
await state.handle.kill().catch(() => { }); // best-effort; the VM destroy reaps anything that survives
|
|
572
825
|
},
|
|
573
826
|
};
|
|
574
827
|
}
|
|
@@ -582,8 +835,11 @@ export class RemoteContainerExecutionEnv {
|
|
|
582
835
|
return this.bgManager.kill(shellId);
|
|
583
836
|
}
|
|
584
837
|
disposeBackgroundShells(opts) {
|
|
838
|
+
// 签名统一带 opts(core 尾调 {except: keepAlive})。隔离 env 的留活是名义的——沙箱拆机进程必然随之
|
|
839
|
+
// 消亡(destroy 整体带走),core 的 envDying settle 会先给准确 killed 回执;透传保证签名不静默丢参。
|
|
585
840
|
return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
|
|
586
841
|
}
|
|
842
|
+
// ─────────────────────────────── FileSystem ───────────────────────────────
|
|
587
843
|
async absolutePath(p) {
|
|
588
844
|
return ok(this.resolve(p));
|
|
589
845
|
}
|
|
@@ -608,7 +864,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
608
864
|
return r;
|
|
609
865
|
let lines = r.value.split(/\r?\n/);
|
|
610
866
|
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
611
|
-
lines.pop();
|
|
867
|
+
lines.pop(); // drop trailing newline's empty tail
|
|
612
868
|
if (options?.maxLines != null)
|
|
613
869
|
lines = lines.slice(0, options.maxLines);
|
|
614
870
|
return ok(lines);
|
|
@@ -630,6 +886,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
630
886
|
if (!sbx.ok)
|
|
631
887
|
return sbx;
|
|
632
888
|
try {
|
|
889
|
+
// E2B write creates parent directories automatically. Uint8Array → its backing ArrayBuffer slice.
|
|
633
890
|
const data = typeof content === "string" ? content : toArrayBuffer(content);
|
|
634
891
|
await this.rpc("files.write", (signal) => sbx.value.files.write(this.resolve(p), data, { signal }), abortSignal, this.cfg.dataTimeoutMs);
|
|
635
892
|
return ok(undefined);
|
|
@@ -639,6 +896,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
639
896
|
}
|
|
640
897
|
}
|
|
641
898
|
async appendFile(p, content, abortSignal) {
|
|
899
|
+
// E2B has no native append; read-modify-write (v1 single-worker → no concurrent-append race).
|
|
642
900
|
const abs = this.resolve(p);
|
|
643
901
|
const existing = await this.readBinaryFile(abs, abortSignal);
|
|
644
902
|
let base;
|
|
@@ -681,6 +939,9 @@ export class RemoteContainerExecutionEnv {
|
|
|
681
939
|
return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
|
|
682
940
|
}
|
|
683
941
|
}
|
|
942
|
+
/** 见其余 shell 腿上同名方法的旁注(2026-07-25 六腿补齐):core 的敏感路径门在 `canonicalPath` 失败时退到它;
|
|
943
|
+
* 缺它 ⇒ 合法的悬空链接让整个工具调用失败。必须返回**原始链接目标**(`readlink` 不带 `-f`)。
|
|
944
|
+
* 本腿的文件面也走 `exec`(与 canonicalPath 同通道),所以实现方式与 shell 腿一致。 */
|
|
684
945
|
async readLink(p, abortSignal) {
|
|
685
946
|
const abs = this.resolve(p);
|
|
686
947
|
const r = await this.exec(`readlink -- ${shellQuote(abs)}`, { abortSignal });
|
|
@@ -696,6 +957,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
696
957
|
if (!r.ok)
|
|
697
958
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
698
959
|
if (r.value.exitCode !== 0) {
|
|
960
|
+
// realpath can fail for more than missing paths — classify like fileError() instead of collapsing to not_found.
|
|
699
961
|
const msg = r.value.stderr.trim();
|
|
700
962
|
if (/permission denied|access denied/i.test(msg))
|
|
701
963
|
return { ok: false, error: new FileError("permission_denied", msg, abs) };
|
|
@@ -716,7 +978,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
716
978
|
catch (e) {
|
|
717
979
|
const fe = this.fileError(e, this.resolve(p), abortSignal);
|
|
718
980
|
if (fe.code === "not_found")
|
|
719
|
-
return ok(false);
|
|
981
|
+
return ok(false); // missing path is `false`, not an error (contract)
|
|
720
982
|
return { ok: false, error: fe };
|
|
721
983
|
}
|
|
722
984
|
}
|
|
@@ -725,6 +987,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
725
987
|
if (!sbx.ok)
|
|
726
988
|
return sbx;
|
|
727
989
|
try {
|
|
990
|
+
// E2B makeDir creates necessary parent directories; returns false if it already exists (treated as ok).
|
|
728
991
|
await this.rpc("files.makeDir", (signal) => sbx.value.files.makeDir(this.resolve(p), { signal }), options?.abortSignal);
|
|
729
992
|
return ok(undefined);
|
|
730
993
|
}
|
|
@@ -738,6 +1001,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
738
1001
|
return sbx;
|
|
739
1002
|
const abs = this.resolve(p);
|
|
740
1003
|
if (options?.recursive) {
|
|
1004
|
+
// E2B files.remove does not guarantee recursive directory removal → shell rm -rf.
|
|
741
1005
|
const flags = options.force ? "-rf" : "-r";
|
|
742
1006
|
const r = await this.exec(`rm ${flags} -- ${shellQuote(abs)}`, { abortSignal: options.abortSignal });
|
|
743
1007
|
if (!r.ok)
|
|
@@ -768,9 +1032,17 @@ export class RemoteContainerExecutionEnv {
|
|
|
768
1032
|
const r = await this.exec(`mktemp ${suffixArg} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal: options?.abortSignal });
|
|
769
1033
|
return this.tempResult(r, "mktemp");
|
|
770
1034
|
}
|
|
1035
|
+
// ─────────────────────────────── internals ───────────────────────────────
|
|
771
1036
|
resolve(p) {
|
|
772
1037
|
return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
|
|
773
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* 🔴 (hang class-fix) Bound a CONTROL-PLANE provider RPC (create/resume/pause/kill/isRunning/files.*) with a
|
|
1041
|
+
* client-side wall-clock deadline + the caller's AbortSignal. Without this, an E2B RPC that hangs (the original
|
|
1042
|
+
* bug: ESTABLISHED socket, no response) pins the worker for the whole sandbox lifetime. On deadline we abort the
|
|
1043
|
+
* op and throw a typed, retryable {@link RemoteExecutionError} code `"timeout"` (caller retries per idempotency —
|
|
1044
|
+
* the adapter NEVER blind-retries). A caller AbortSignal still aborts independently (op surfaces it / "aborted").
|
|
1045
|
+
*/
|
|
774
1046
|
async rpc(label, op, userSignal, timeoutMs = this.cfg.rpcTimeoutMs) {
|
|
775
1047
|
const deadline = new AbortController();
|
|
776
1048
|
const timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
@@ -781,24 +1053,55 @@ export class RemoteContainerExecutionEnv {
|
|
|
781
1053
|
reject(new RemoteExecutionError("timeout", `remote ${label} exceeded ${timeoutMs}ms (provider RPC hang)`));
|
|
782
1054
|
}, { once: true }));
|
|
783
1055
|
try {
|
|
1056
|
+
// RACE the op against the deadline: even if the provider SDK ignores `signal` (e.g. create/isRunning/kill),
|
|
1057
|
+
// the worker is freed on timeout (the op may leak in the background until the SDK's own timeout — but it no
|
|
1058
|
+
// longer PINS us for the sandbox lifetime, the original hang bug). Passing `signal` lets SDK methods that DO
|
|
1059
|
+
// honor it (files.*, pause, connect) actually cancel the request. A caller `userSignal` abort propagates via
|
|
1060
|
+
// `signal` → the op rejects with its own abort error (NOT surfaced as "timeout"; deadline never fired).
|
|
784
1061
|
return await Promise.race([op(signal), onDeadline]);
|
|
785
1062
|
}
|
|
786
1063
|
finally {
|
|
787
1064
|
clearTimeout(timer);
|
|
788
|
-
combined?.dispose();
|
|
1065
|
+
combined?.dispose(); // council BUG-2: release the fallback anySignal listeners (no-op for AbortSignal.any)
|
|
789
1066
|
}
|
|
790
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Per-command wall-clock for the E2B SDK (seconds→ms). 🔴 This is a PHYSICAL CAP, NOT the hang
|
|
1070
|
+
* detector. The old code returned `this.cfg.timeoutMs` (the sandbox lifetime) when no `timeout` was given AND
|
|
1071
|
+
* had no liveness check → a hung command waited the WHOLE lifetime (~30 min, the original hang bug). The fix is
|
|
1072
|
+
* NOT to shorten this cap (that would spuriously kill a long-but-progressing `npm ci`) — it's to add a LIVENESS
|
|
1073
|
+
* bound ({@link cfg.livenessMs}) in exec/execStream that catches a *zero-progress* command in ~livenessMs,
|
|
1074
|
+
* independent of this cap. So: explicit caller `timeout` wins; else the lifetime is the cap (a command can't
|
|
1075
|
+
* outlive its VM) but hang detection no longer depends on it.
|
|
1076
|
+
*/
|
|
791
1077
|
commandTimeoutMs(timeoutSec) {
|
|
792
1078
|
return timeoutSec != null ? timeoutSec * 1000 : this.cfg.timeoutMs;
|
|
793
1079
|
}
|
|
794
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* 🔴 Liveness for the buffered exec WAIT phase. `handle.wait()` is a long-poll with no read timeout —
|
|
1082
|
+
* a command that hangs DURING execution (not creation) would otherwise wait the E2B command timeout (= the
|
|
1083
|
+
* lifetime when none is set, the original hang bug). Races wait() against an idle check: if no stdout/stderr for
|
|
1084
|
+
* `livenessMs`, the command is hung → kill + typed retryable "timeout". `lastProgress()` is bumped by the
|
|
1085
|
+
* stdout/stderr callbacks, so a slow-but-PROGRESSING command (long `npm ci`) is never killed. (execStream has
|
|
1086
|
+
* its own chunk-driven idle timer; this is the buffered-exec analogue.)
|
|
1087
|
+
*/
|
|
1088
|
+
async waitWithLiveness(handle, lastProgress,
|
|
1089
|
+
// design/116 detach: `waitP` shares the ONE `handle.wait()` consumption with exec's adoption path; `disarmed`
|
|
1090
|
+
// (⇒ the command was adopted as a background shell) makes the liveness/wall timers dead switches — the fg leg
|
|
1091
|
+
// must NEVER kill a process the bgManager now owns; `wallMs` is the control-plane foreground wall for the
|
|
1092
|
+
// detach-capable path (whose provider timeout was lifted to the BG ceiling).
|
|
1093
|
+
opts) {
|
|
795
1094
|
let timer;
|
|
796
1095
|
let wallTimer;
|
|
1096
|
+
// council DESIGN-1: a fixed 5s polling setInterval can fire AT the deadline and kill a command that just
|
|
1097
|
+
// finished (race vs handle.wait settling, discarding its exitCode). Instead arm a setTimeout for EXACTLY the
|
|
1098
|
+
// remaining idle window; when it fires, re-check lastProgress() — if output advanced it, re-arm for the new
|
|
1099
|
+
// remainder (so a slow-but-progressing command is never killed), else it is genuinely idle → kill + timeout.
|
|
797
1100
|
const idle = new Promise((_, reject) => {
|
|
798
1101
|
const arm = () => {
|
|
799
1102
|
timer = setTimeout(() => {
|
|
800
1103
|
if (opts?.disarmed?.())
|
|
801
|
-
return;
|
|
1104
|
+
return; // adopted: the BG wall owns the process now — never kill from the fg leg
|
|
802
1105
|
const idleFor = Date.now() - lastProgress();
|
|
803
1106
|
if (idleFor >= this.cfg.livenessMs) {
|
|
804
1107
|
void handle.kill().catch(() => { });
|
|
@@ -806,7 +1109,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
806
1109
|
reject(new RemoteExecutionError("timeout", `exec idle > ${this.cfg.livenessMs}ms (provider hang / unreachable sandbox)`));
|
|
807
1110
|
}
|
|
808
1111
|
else {
|
|
809
|
-
arm();
|
|
1112
|
+
arm(); // progress advanced lastProgress since arming → re-arm for the now-remaining window
|
|
810
1113
|
}
|
|
811
1114
|
}, Math.max(this.cfg.livenessMs - (Date.now() - lastProgress()), 0));
|
|
812
1115
|
};
|
|
@@ -821,6 +1124,8 @@ export class RemoteContainerExecutionEnv {
|
|
|
821
1124
|
}
|
|
822
1125
|
});
|
|
823
1126
|
try {
|
|
1127
|
+
// handle.wait() settling (resolve OR CommandExitError reject) wins the race over a still-pending timer, so a
|
|
1128
|
+
// command that completes is never killed; finally clears the timer on every exit path.
|
|
824
1129
|
return await Promise.race([opts?.waitP ?? handle.wait(), idle]);
|
|
825
1130
|
}
|
|
826
1131
|
finally {
|
|
@@ -830,7 +1135,10 @@ export class RemoteContainerExecutionEnv {
|
|
|
830
1135
|
clearTimeout(wallTimer);
|
|
831
1136
|
}
|
|
832
1137
|
}
|
|
1138
|
+
/** Lazily provision the VM on first use: a task that never touches the hand pays zero cost.
|
|
1139
|
+
* Concurrent first-calls share one provision via the memoized {@link connect}. */
|
|
833
1140
|
async ensureConnected() {
|
|
1141
|
+
// A paused (suspended) env's this.sbx is a dead handle — fail clearly rather than running fs/exec against it.
|
|
834
1142
|
if (this.paused)
|
|
835
1143
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resume before fs/exec") };
|
|
836
1144
|
if (this.sbx)
|
|
@@ -872,10 +1180,16 @@ export class RemoteContainerExecutionEnv {
|
|
|
872
1180
|
const name = e?.name;
|
|
873
1181
|
if (abortSignal?.aborted || name === "AbortError")
|
|
874
1182
|
return new FileError("aborted", msg, p);
|
|
1183
|
+
// A remote-fs RPC liveness timeout arrives as RemoteExecutionError("timeout"); surface it on the FileSystem
|
|
1184
|
+
// seam as the symmetric FileError("timeout") (core 1.77.0 added "timeout" to FileErrorCode too — [R71]) so the
|
|
1185
|
+
// retryable-hang signal travels as a typed code, not just a message. + a metric so a file-RPC hang stays
|
|
1186
|
+
// observable. Minor: file RPCs rarely hang and only past dataTimeoutMs (5m).
|
|
875
1187
|
if (e instanceof RemoteExecutionError && e.code === "timeout") {
|
|
876
1188
|
this.cfg.metrics?.inc("remote_env_file_timeout_total", { provider: PROVIDER });
|
|
877
1189
|
return new FileError("timeout", msg, p, e);
|
|
878
1190
|
}
|
|
1191
|
+
// The sandbox itself vanishing (reaped/timed-out) is an INFRASTRUCTURE failure, not a confirmed-absent
|
|
1192
|
+
// file — never collapse it to not_found, or exists()/stat() would report a dead VM as "file is absent".
|
|
879
1193
|
if (name === "SandboxNotFoundError" || /sandbox .*not found|sandbox .*not running|sandbox was not found/i.test(msg)) {
|
|
880
1194
|
return new FileError("unknown", msg, p);
|
|
881
1195
|
}
|
|
@@ -909,15 +1223,30 @@ export class RemoteContainerExecutionEnv {
|
|
|
909
1223
|
return new RemoteExecutionError(code, errMsg(e), e instanceof Error ? e : undefined);
|
|
910
1224
|
}
|
|
911
1225
|
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Build the deployment-level `RunnerDeps.executionEnvFactory` (design/48 v1b). Wiring this onto a
|
|
1228
|
+
* deployment makes it a "code-agent worker": every task's hand runs in a per-task E2B VM. Deployments that do
|
|
1229
|
+
* NOT set this (e.g. OA / review) keep the in-process stub env — routing is **deployment-level** (the factory
|
|
1230
|
+
* ctx carries only sessionId/taskId, never a service scenario; that stays out of the generic core seam).
|
|
1231
|
+
*
|
|
1232
|
+
* The factory returns the env **unconnected** (lazy): the VM is provisioned on first FS/Shell/execStream use,
|
|
1233
|
+
* so a task that never touches the hand (e.g. a leader that only plans) pays zero VM cost. The Runner owns the
|
|
1234
|
+
* lifetime and calls `destroy()` on task end (via `hasDestroy`); a never-used env's destroy is a no-op.
|
|
1235
|
+
*/
|
|
912
1236
|
export function e2bExecutionEnvFactory(config, deps) {
|
|
913
1237
|
return (ctx) => new RemoteContainerExecutionEnv({
|
|
914
1238
|
...config,
|
|
915
1239
|
metadata: { ...config.metadata, sessionId: ctx.sessionId, ...(ctx.taskId ? { taskId: ctx.taskId } : {}) },
|
|
916
1240
|
}, deps);
|
|
917
1241
|
}
|
|
1242
|
+
// ─────────────────────────────── module helpers ───────────────────────────────
|
|
918
1243
|
function errMsg(e) {
|
|
919
1244
|
return e instanceof Error ? e.message : String(e);
|
|
920
1245
|
}
|
|
1246
|
+
/** E2B FilesystemRequestOpts `{ signal }` spread helper (omit the key entirely when no signal). */
|
|
1247
|
+
/** Combine signals into one that aborts when ANY input does (used by {@link RemoteContainerExecutionEnv.rpc} to
|
|
1248
|
+
* race the caller's abort with the client-side deadline). Uses the platform `AbortSignal.any` when present
|
|
1249
|
+
* (Node ≥20 / Bun), else a small fallback. */
|
|
921
1250
|
function anySignal(signals) {
|
|
922
1251
|
const ctor = AbortSignal;
|
|
923
1252
|
if (typeof ctor.any === "function")
|
|
@@ -931,13 +1260,15 @@ function anySignal(signals) {
|
|
|
931
1260
|
break;
|
|
932
1261
|
}
|
|
933
1262
|
s.addEventListener("abort", onAbort, { once: true });
|
|
934
|
-
cleanups.push(() => s.removeEventListener("abort", onAbort));
|
|
1263
|
+
cleanups.push(() => s.removeEventListener("abort", onAbort)); // council BUG-2: dispose() removes these on long-lived signals
|
|
935
1264
|
}
|
|
936
1265
|
return { signal: ctrl.signal, dispose: () => cleanups.forEach((c) => c()) };
|
|
937
1266
|
}
|
|
1267
|
+
/** POSIX single-quote a string for safe shell interpolation. */
|
|
938
1268
|
function shellQuote(s) {
|
|
939
1269
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
940
1270
|
}
|
|
1271
|
+
/** Copy a Uint8Array's exact bytes into a standalone ArrayBuffer (E2B write accepts ArrayBuffer). */
|
|
941
1272
|
function toArrayBuffer(u) {
|
|
942
1273
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
943
1274
|
}
|