@sema-agent/server 1.323.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +58 -2
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RemoteK8sExecutionEnv` — the Kubernetes pod-sandbox implementation of core's `RemoteExecutionEnv` seam
|
|
3
|
+
* (design/61). A PEER adapter to E2B/SSH/ADB (NOT chained). Use case: self-hosted sandboxes on our k3s with
|
|
4
|
+
* **Kata Containers** (`runtimeClassName: kata-qemu`) — a real VM boundary (guest kernel ≠ host, proven in the
|
|
5
|
+
* 2026-06-10 PoC) at ~2s startup, par with managed E2B.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 Capabilities: `{ isolation: true, suspendable: false }` — Kata gives VM isolation, but
|
|
8
|
+
* there is NO managed memory snapshot (E2B-style pause/resume). `suspendVM`/`resumeVM`/`postResumeInit` →
|
|
9
|
+
* typed `"unsupported"` so durable suspend degrades honestly (checkpoint + cold rebuild, in-VM process state
|
|
10
|
+
* like a language server is lost). True memory snapshots = a separate CRIU/Kata-snapshot project, not implied.
|
|
11
|
+
*
|
|
12
|
+
* Transport: the k8s REST API + the exec WebSocket (channel-multiplexed: 1-byte channel prefix per frame —
|
|
13
|
+
* 1=stdout 2=stderr 3=status JSON) spoken directly over `ws` — deliberately NO `@kubernetes/client-node`
|
|
14
|
+
* dependency (its exec path is the only thing we'd use; our liveness/timeout discipline plugs straight into a
|
|
15
|
+
* raw socket, and the fault-injection seam stays trivial). Unlike `adb exec-out`, the status channel carries a
|
|
16
|
+
* REAL exit code.
|
|
17
|
+
*
|
|
18
|
+
* 🔴 Subprotocol = `v5.channel.k8s.io` first, `v4` fallback (night-drill 2026-06-11): the apiserver's **v4**
|
|
19
|
+
* WebSocket exec writer STALLS after the first ~6KB of a fast output burst (verified: the TCP socket stays
|
|
20
|
+
* healthy/flowing, the apiserver just stops sending; v5 streams the same 1MB+ burst to completion). v5 (k8s
|
|
21
|
+
* 1.29+) is what modern kubectl negotiates; the frame format is identical (it only adds a close-channel we
|
|
22
|
+
* ignore), so offering both lets a v5 apiserver fix the stall while a v4-only one still works for small output.
|
|
23
|
+
*
|
|
24
|
+
* Timeouts follow the E2B class-fix taxonomy: control-plane RPCs get a short wall-clock
|
|
25
|
+
* (`rpcTimeoutMs`); commands get a zero-progress LIVENESS bound (`livenessMs`) plus the caller's explicit
|
|
26
|
+
* wall-clock; pod readiness gets its own generous bound (image pull can be slow). ⚠️ killing the exec socket
|
|
27
|
+
* does NOT kill the remote process — acceptable here because the pod is DISPOSABLE (destroyed with the task;
|
|
28
|
+
* `activeDeadlineSeconds` is the leak guard), unlike the SSH real-machine posture.
|
|
29
|
+
*
|
|
30
|
+
* Auth: a bearer token + CA, either explicit (`K8S_API_URL`/`K8S_TOKEN`/`K8S_CA_CERT_B64`) or in-cluster
|
|
31
|
+
* (service-account files + `KUBERNETES_SERVICE_HOST`) — control-plane config, never agent-reachable.
|
|
32
|
+
*/
|
|
1
33
|
import path from "node:path";
|
|
2
34
|
import { readFileSync } from "node:fs";
|
|
3
35
|
import https from "node:https";
|
|
@@ -12,13 +44,25 @@ import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
|
12
44
|
import { randomUUID } from "node:crypto";
|
|
13
45
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
14
46
|
const PROVIDER = "k8s";
|
|
47
|
+
/**
|
|
48
|
+
* design/103 background-shell (TOB, path ① — pod-side file buffer re-read by byte cursor). Bounds for kata/k8s.
|
|
49
|
+
* A background job cannot outlive its pod (`activeDeadlineSeconds`), so `maxBgTimeoutSec` is additionally clamped
|
|
50
|
+
* to the pod lifetime at construction. `maxConcurrent` mirrors the TOC default (8) — anti fork-bomb.
|
|
51
|
+
*/
|
|
15
52
|
const K8S_BG_DIR_ROOT = "/tmp/.sema-bg";
|
|
16
53
|
const K8S_BG_MAX_CONCURRENT = numEnvOr("K8S_BG_MAX_CONCURRENT", 8, 1);
|
|
17
54
|
const K8S_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("K8S_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
|
|
18
55
|
const K8S_BG_MAX_TIMEOUT_SEC = numEnvOr("K8S_BG_MAX_TIMEOUT_SEC", 1800, K8S_BG_DEFAULT_TIMEOUT_SEC);
|
|
56
|
+
/** Max bytes returned per stream per poll (bounds the control-plane transfer; the model only displays ~20K anyway). */
|
|
19
57
|
const K8S_BG_READ_CAP = numEnvOr("K8S_BG_READ_CAP", 1024 * 1024, 64 * 1024);
|
|
58
|
+
/** Per-stream pod-side file cap (FIFO+`head` — a runaway `yes` is SIGPIPE'd at this bound; the pod has no ephemeral-storage limit). */
|
|
20
59
|
const K8S_BG_FILE_CAP = numEnvOr("K8S_BG_FILE_CAP", 64 * 1024 * 1024, K8S_BG_READ_CAP);
|
|
60
|
+
/** Per-stream control-plane drain cap for an ADOPTED (mid-flight detach) shell — its output rides the still-open
|
|
61
|
+
* exec WebSocket into worker memory, not pod files, so bound the un-polled queue (drop-oldest → `truncated`). */
|
|
21
62
|
const K8S_BG_MEM_CAP = numEnvOr("K8S_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
|
|
63
|
+
// design/158 A13: the pure background-shell pod-side script generation/parsing group (buildBg*Script ×5 +
|
|
64
|
+
// buildDetachCapableExec + parseBgPollOutput + ParsedBgPoll) moved VERBATIM to ./k8s-bg-scripts.js —
|
|
65
|
+
// value-imported below (the class body still calls them) and re-exported so existing importers keep working.
|
|
22
66
|
import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
|
|
23
67
|
export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
|
|
24
68
|
const ok = (value) => ({ ok: true, value });
|
|
@@ -27,12 +71,29 @@ const unsupported = (op) => ({
|
|
|
27
71
|
error: new RemoteExecutionError("unsupported", `${op} is not supported on a Kata pod sandbox (no managed memory snapshot — capabilities.suspendable=false; durable suspend degrades to checkpoint + cold rebuild)`),
|
|
28
72
|
});
|
|
29
73
|
const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
|
|
74
|
+
/** Exec WebSocket subprotocols, preference order (see header — v4 burst-stall). */
|
|
30
75
|
const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
|
|
76
|
+
/**
|
|
77
|
+
* argv-embedded base64 write chunk (SOURCE bytes). 🔴 The bound is Linux `MAX_ARG_STRLEN` = 128KiB (131072),
|
|
78
|
+
* the limit on a SINGLE argv string — and the whole `printf '%s' '<base64>' | base64 -d >> '<path>'` command
|
|
79
|
+
* is passed as one `sh -c <cmd>` argument. base64 inflates 4/3, so a 96KiB source chunk made a 131072-byte
|
|
80
|
+
* base64 = exactly the limit ⇒ silent truncation (night-drill: writes >95KiB failed "command failed"). 64KiB
|
|
81
|
+
* source → 87KiB base64 + wrapper ≈ 87KiB command, a safe ~44KiB under the limit.
|
|
82
|
+
*/
|
|
31
83
|
const WRITE_CHUNK_BYTES = 64 * 1024;
|
|
84
|
+
// design/158 A13: the pod-spec patch group (PodSpecPatch + applyPodSpecPatch + its hardening/volume guards)
|
|
85
|
+
// moved VERBATIM to ./k8s-exec-protocol.js — value-imported below (the class body calls applyPodSpecPatch)
|
|
86
|
+
// and re-exported so existing importers keep working.
|
|
32
87
|
import { applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
33
88
|
export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
89
|
+
// design/158 A13: execSocketTlsOptions + exitCodeFromStatus moved VERBATIM to ./k8s-exec-protocol.js
|
|
90
|
+
// (errorMessageOf, sandwiched between them in the original layout, stays HERE — it is a small ws-error
|
|
91
|
+
// helper, not part of the pod-spec-patch/protocol group). Value-imported below (the class body calls both)
|
|
92
|
+
// and re-exported so existing importers keep working.
|
|
34
93
|
import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
|
|
35
94
|
export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
|
|
95
|
+
/** `Error`-or-event → message. Bun's ws shim emits browser-style ErrorEvents (NOT instanceof Error) whose
|
|
96
|
+
* String() is "[object ErrorEvent]" — that masked the real TLS failure for a whole drill. */
|
|
36
97
|
export function errorMessageOf(e) {
|
|
37
98
|
if (e instanceof Error)
|
|
38
99
|
return e.message;
|
|
@@ -44,10 +105,15 @@ export class RemoteK8sExecutionEnv {
|
|
|
44
105
|
capabilities;
|
|
45
106
|
cwd;
|
|
46
107
|
podName;
|
|
108
|
+
/** Cluster-internal pod address, captured when the pod reaches Running — the LSP `getHost` surface.
|
|
109
|
+
* Lifecycle mirrors podName exactly (set on connect, cleared on suspend/destroy/dead-reconnect). */
|
|
47
110
|
podIP;
|
|
48
111
|
destroyed = false;
|
|
112
|
+
/** Set after a successful resumeVM so workspaceHandle() carries the snapshot it was restored from. */
|
|
49
113
|
handleSnapshotId;
|
|
114
|
+
/** In-flight exec count — `suspendVM` refuses while a command runs (council #4 suspend×in-flight). */
|
|
50
115
|
inFlightExecs = 0;
|
|
116
|
+
/** Memoized in-flight connect (lazy-connect; prevents concurrent double pod-create — the E2B race lesson). */
|
|
51
117
|
connectPromise;
|
|
52
118
|
cfg;
|
|
53
119
|
deps;
|
|
@@ -72,17 +138,24 @@ export class RemoteK8sExecutionEnv {
|
|
|
72
138
|
...(config.s3Snapshot ? { s3Snapshot: config.s3Snapshot } : {}),
|
|
73
139
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
74
140
|
...(config.podSpecPatch ? { podSpecPatch: config.podSpecPatch } : {}),
|
|
75
|
-
...(config.podEnv ? { podEnv: config.podEnv } : {}),
|
|
141
|
+
...(config.podEnv ? { podEnv: config.podEnv } : {}), // RFC B5: region package-source env
|
|
76
142
|
};
|
|
77
143
|
this.cwd = this.cfg.mountPath;
|
|
78
144
|
this.deps = deps ?? {};
|
|
145
|
+
// Workspace snapshots make this env suspendable (files durable across suspend/resume); without an S3
|
|
146
|
+
// store it stays the honest checkpoint + cold-rebuild degrade.
|
|
79
147
|
this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot };
|
|
148
|
+
// Clamp BOTH the ceiling AND the default to the pod lifetime — a short-lived pod must not advertise a default
|
|
149
|
+
// bigger than its max (the manager would clamp the actual spawn, making the advertised default a lie).
|
|
80
150
|
const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
|
|
81
151
|
this.backgroundCapabilities = {
|
|
82
152
|
supported: true,
|
|
83
153
|
maxConcurrent: K8S_BG_MAX_CONCURRENT,
|
|
84
154
|
defaultBgTimeoutSec: Math.min(K8S_BG_DEFAULT_TIMEOUT_SEC, bgMax),
|
|
85
155
|
maxBgTimeoutSec: bgMax,
|
|
156
|
+
// core 1.207 design/116 detach (1.82): a running foreground exec is ADOPTED as a background shell
|
|
157
|
+
// (mid-flight ctrl+b) — the exec WebSocket keeps streaming into memory-mode drain buffers; kill works
|
|
158
|
+
// through the pgid the detach-capable exec prelude self-reported (buildDetachCapableExec).
|
|
86
159
|
supportsDetach: true,
|
|
87
160
|
};
|
|
88
161
|
}
|
|
@@ -92,6 +165,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
92
165
|
workspaceHandle() {
|
|
93
166
|
return { sandboxId: this.podName ?? "(unconnected)", provider: PROVIDER, mountPath: this.cwd, ...(this.handleSnapshotId ? { snapshotId: this.handleSnapshotId } : {}) };
|
|
94
167
|
}
|
|
168
|
+
// ─────────────────────────────── auth / transport ───────────────────────────────
|
|
169
|
+
/** Resolved lazily so an in-cluster pod picks up its service account without explicit config. */
|
|
95
170
|
creds() {
|
|
96
171
|
const apiUrl = this.cfg.apiUrl ??
|
|
97
172
|
(process.env.KUBERNETES_SERVICE_HOST ? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT ?? "443"}` : undefined);
|
|
@@ -117,6 +192,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
117
192
|
}
|
|
118
193
|
return { apiUrl: apiUrl.replace(/\/+$/, ""), token, ca };
|
|
119
194
|
}
|
|
195
|
+
/** Control-plane REST call (pod create/get/delete), bounded by rpcTimeoutMs. Injectable for tests. */
|
|
120
196
|
async request(method, apiPath, body, opts) {
|
|
121
197
|
if (this.deps.request)
|
|
122
198
|
return this.deps.request(method, apiPath, body, opts);
|
|
@@ -171,15 +247,22 @@ export class RemoteK8sExecutionEnv {
|
|
|
171
247
|
...(url.startsWith("wss:") ? execSocketTlsOptions(ca, this.cfg.insecureTls) : {}),
|
|
172
248
|
});
|
|
173
249
|
}
|
|
250
|
+
// ─────────────────────────────── lifecycle ───────────────────────────────
|
|
174
251
|
async connect(config) {
|
|
175
252
|
const policy = {
|
|
176
|
-
retryableCodes: ["connect_failed", "timeout", "transport_lost"],
|
|
253
|
+
retryableCodes: ["connect_failed", "timeout", "transport_lost"], // NOT auth_failed
|
|
177
254
|
maxAttempts: this.cfg.retry.maxAttempts,
|
|
178
255
|
backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
|
|
179
256
|
};
|
|
180
257
|
const once = this.deps.connectOnce ?? ((s) => this.connectOnce(s));
|
|
181
258
|
return withRetry(() => once(config?.abortSignal), policy, { signal: config?.abortSignal });
|
|
182
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Lazy-connect on first use — 🔴 core NEVER calls `connect()` (verified: the Runner only calls `resumeVM` on
|
|
262
|
+
* the resume path), so an adapter that demands an explicit connect can never work through the production
|
|
263
|
+
* hand-tool path (lazy-connect belongs in the adapter). Memoized so concurrent first ops share ONE
|
|
264
|
+
* pod create; a failed connect clears the memo (next op retries); `destroyed` is terminal.
|
|
265
|
+
*/
|
|
183
266
|
async ensureConnected() {
|
|
184
267
|
if (this.podName)
|
|
185
268
|
return ok(undefined);
|
|
@@ -188,7 +271,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
188
271
|
this.connectPromise ??= this.connect();
|
|
189
272
|
const r = await this.connectPromise;
|
|
190
273
|
if (!r.ok) {
|
|
191
|
-
this.connectPromise = undefined;
|
|
274
|
+
this.connectPromise = undefined; // don't cache the failure — the next op gets a fresh attempt
|
|
192
275
|
return { ok: false, error: new ExecutionError("shell_unavailable", `k8s sandbox connect failed: ${r.error.message}`, r.error) };
|
|
193
276
|
}
|
|
194
277
|
return ok(undefined);
|
|
@@ -209,16 +292,24 @@ export class RemoteK8sExecutionEnv {
|
|
|
209
292
|
spec: {
|
|
210
293
|
...(this.cfg.runtimeClass ? { runtimeClassName: this.cfg.runtimeClass } : {}),
|
|
211
294
|
restartPolicy: "Never",
|
|
295
|
+
// Defense-in-depth (council DESIGN#1): the agent runs INSIDE this pod; never mount a k8s SA token into
|
|
296
|
+
// it. The adapter holds the only credential it needs (control-plane side) — the workload gets none, so
|
|
297
|
+
// an accidental cluster-wide default-SA binding can't hand the agent the API.
|
|
212
298
|
automountServiceAccountToken: false,
|
|
299
|
+
// Leak guard (the E2B-lifetime analogue): the pod self-terminates even if destroy() never runs.
|
|
213
300
|
activeDeadlineSeconds: Math.ceil(this.cfg.timeoutMs / 1000),
|
|
214
301
|
terminationGracePeriodSeconds: 1,
|
|
215
302
|
containers: [
|
|
216
303
|
{
|
|
217
304
|
name: "sandbox",
|
|
218
305
|
image: this.cfg.image,
|
|
306
|
+
// RFC B5: when a pkg-source profile is injected, run the image's materializer ONCE at pod start
|
|
307
|
+
// (root, before any exec — core's exec lanes are non-login `sh -c`, so the profile.d hook alone
|
|
308
|
+
// wouldn't fire there). Script-absent images no-op; podEnv unset ⇒ command byte-identical to today.
|
|
219
309
|
command: this.cfg.podEnv?.SEMA_PKG_SOURCE
|
|
220
310
|
? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
|
|
221
311
|
: ["sh", "-c", "sleep infinity"],
|
|
312
|
+
// RFC B5: operator-trusted region env (pkg-source selection). Absent = no env key at all (byte-identical).
|
|
222
313
|
...(this.cfg.podEnv && Object.keys(this.cfg.podEnv).length > 0
|
|
223
314
|
? { env: Object.entries(this.cfg.podEnv).map(([name, value]) => ({ name, value: String(value) })) }
|
|
224
315
|
: {}),
|
|
@@ -227,6 +318,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
227
318
|
],
|
|
228
319
|
},
|
|
229
320
|
};
|
|
321
|
+
// 🔴 ADDITIVE deployment patch: add RO oracle mounts / hardened securityContext for the L8
|
|
322
|
+
// immutable-mount probe + the graderEnvFactory. Applied AFTER the hard invariants above so it can never weaken
|
|
323
|
+
// them (applyPodSpecPatch only touches volumes/mounts/securityContext/labels). No-op when unset (byte-identical).
|
|
230
324
|
if (this.cfg.podSpecPatch)
|
|
231
325
|
applyPodSpecPatch(pod, this.cfg.podSpecPatch);
|
|
232
326
|
let created;
|
|
@@ -245,6 +339,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
245
339
|
const name = created.json?.metadata?.name;
|
|
246
340
|
if (!name)
|
|
247
341
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "pod create returned no name") };
|
|
342
|
+
// Poll until Running (schedule + image pull + Kata VM boot), bounded by readyTimeoutMs.
|
|
248
343
|
const deadline = Date.now() + this.cfg.readyTimeoutMs;
|
|
249
344
|
let podIP;
|
|
250
345
|
for (;;) {
|
|
@@ -281,12 +376,16 @@ export class RemoteK8sExecutionEnv {
|
|
|
281
376
|
}
|
|
282
377
|
await new Promise((r) => setTimeout(r, 1000));
|
|
283
378
|
}
|
|
379
|
+
// Destroy-race guard (the E2B lazy-connect BLOCKER lesson): if destroy() ran while we were creating the
|
|
380
|
+
// pod, kill the fresh pod and bail — never resurrect a destroyed env (destroyed is terminal).
|
|
284
381
|
if (this.destroyed) {
|
|
285
382
|
await this.deletePod(name).catch(() => { });
|
|
286
383
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "env destroyed during connect") };
|
|
287
384
|
}
|
|
288
385
|
this.podName = name;
|
|
289
386
|
this.podIP = podIP;
|
|
387
|
+
// Workspace root (the image may not ship it) — via runExec DIRECTLY: this.exec() would re-enter
|
|
388
|
+
// ensureConnected and deadlock on the still-pending connect memo. Failure = unusable sandbox.
|
|
290
389
|
const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}`);
|
|
291
390
|
if (!mk.ok || mk.value.exitCode !== 0) {
|
|
292
391
|
await this.deletePod(name).catch(() => { });
|
|
@@ -296,6 +395,14 @@ export class RemoteK8sExecutionEnv {
|
|
|
296
395
|
}
|
|
297
396
|
return ok(this.workspaceHandle());
|
|
298
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Build a validated, presigned URL for a snapshot object — `<prefix>/<sessionId>/<snapshotId>.tar.gz`.
|
|
400
|
+
* 🔴 sessionId comes from the client (`body.sessionId`, security.ts) and is NOT charset-validated upstream, so
|
|
401
|
+
* each path segment is allowlisted here (`^[A-Za-z0-9][A-Za-z0-9._-]*$`) before it enters the object key: a
|
|
402
|
+
* segment of `..` / `../x` / one containing `/` would otherwise let one tenant's snapshot key normalize into
|
|
403
|
+
* another tenant's prefix (cross-tenant read on resume / clobber on suspend). Returns a typed error instead of
|
|
404
|
+
* signing a traversing key. Folds the old snapshotKey()+s3Presign() pair so the two call sites can't diverge.
|
|
405
|
+
*/
|
|
299
406
|
presignSnapshot(method, snapshotId) {
|
|
300
407
|
const s3 = this.cfg.s3Snapshot;
|
|
301
408
|
const prefix = s3.keyPrefix ?? "kata";
|
|
@@ -315,29 +422,48 @@ export class RemoteK8sExecutionEnv {
|
|
|
315
422
|
accessKey: s3.accessKey,
|
|
316
423
|
secretKey: s3.secretKey,
|
|
317
424
|
...(s3.region ? { region: s3.region } : {}),
|
|
425
|
+
// Clamp the presigned-URL TTL to a 1h ceiling — a short-lived single-object credential must not become a
|
|
426
|
+
// long-lived bearer token via an over-large WORKSPACE_SNAPSHOT_TTL_SEC.
|
|
318
427
|
expiresSec: Math.min(s3.presignTtlSec ?? 600, 3600),
|
|
319
428
|
}),
|
|
320
429
|
};
|
|
321
430
|
}
|
|
431
|
+
/** Redact a presigned URL (and any lingering signature) from text before it enters an error/log — the URL is a
|
|
432
|
+
* short-lived bearer credential and must never reach a durable store (secret boundary). */
|
|
322
433
|
redactUrl(text, url) {
|
|
323
434
|
return text.split(url).join("<presigned-url>").replace(/X-Amz-Signature=[0-9a-f]+/gi, "X-Amz-Signature=<redacted>");
|
|
324
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Workspace-durable suspend: tar the workspace, upload to S3 via a short-lived adapter-presigned
|
|
438
|
+
* URL the pod curls (credentials never enter the sandbox), then delete the pod. The returned SnapshotId is
|
|
439
|
+
* opaque to core; we use a fresh id and persist the bytes under {@link snapshotKey}. NOT supported without an
|
|
440
|
+
* S3 store (returns the honest "unsupported" so durable suspend degrades to checkpoint + cold rebuild).
|
|
441
|
+
*
|
|
442
|
+
* 🔴 council #4 (suspend × in-flight exec): refuse while a command runs — tarring a mutating tree would
|
|
443
|
+
* snapshot a torn workspace. Core also won't suspend mid-tool, but the adapter enforces it independently.
|
|
444
|
+
*/
|
|
325
445
|
async suspendVM(_options) {
|
|
326
446
|
if (!this.cfg.s3Snapshot)
|
|
327
447
|
return unsupported("suspendVM");
|
|
328
448
|
if (this.inFlightExecs > 0)
|
|
329
449
|
return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
|
|
330
450
|
const snapshotId = `kata-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
451
|
+
// A never-connected env (a plan-only task that suspended without touching the hand) has no workspace to
|
|
452
|
+
// capture — return a snapshotId with no object; resume tolerates the 404 as an empty workspace.
|
|
331
453
|
if (!this.podName)
|
|
332
454
|
return ok(snapshotId);
|
|
333
455
|
const presigned = this.presignSnapshot("PUT", snapshotId);
|
|
334
456
|
if (!presigned.ok)
|
|
335
457
|
return presigned;
|
|
336
458
|
const url = presigned.value;
|
|
459
|
+
// Materialize the archive to a pod-local temp file, THEN upload — a streamed `tar | curl -T -` sends
|
|
460
|
+
// Transfer-Encoding: chunked with no Content-Length, which MinIO rejects with HTTP 411 on a presigned PUT
|
|
461
|
+
// (live-verified). `curl -T <file>` sets Content-Length from the file size. Bytes still go pod→MinIO (never
|
|
462
|
+
// the worker); the temp file lives in the disposable pod and is removed regardless of curl's outcome.
|
|
337
463
|
const tmp = "/tmp/ws-snap.tgz";
|
|
338
464
|
const cmd = `tar czf ${tmp} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
|
|
339
465
|
`rc=$?; rm -f ${tmp}; exit $rc`;
|
|
340
|
-
const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
|
|
466
|
+
const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 }); // silent bulk upload → wall bound only
|
|
341
467
|
if (!r.ok)
|
|
342
468
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
|
|
343
469
|
if (r.value.exitCode !== 0)
|
|
@@ -345,12 +471,24 @@ export class RemoteK8sExecutionEnv {
|
|
|
345
471
|
const name = this.podName;
|
|
346
472
|
this.podName = undefined;
|
|
347
473
|
this.podIP = undefined;
|
|
348
|
-
await this.deletePod(name).catch(() => { });
|
|
474
|
+
await this.deletePod(name).catch(() => { }); // suspend = release the pod; the snapshot is the durable state
|
|
349
475
|
return ok(snapshotId);
|
|
350
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
|
|
479
|
+
* provision a pod, then download + untar the snapshot into the workspace. A missing object (empty/plan-only
|
|
480
|
+
* snapshot) resumes as a clean workspace.
|
|
481
|
+
*
|
|
482
|
+
* [1467]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
|
|
483
|
+
* 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
|
|
484
|
+
* 触顶即 pod 驱逐(恢复中途死,比「根变了 fail-closed reopen」更糟)。所以 untar 落当前根、handle 回报
|
|
485
|
+
* 真生效根(core 契约明许:「做不到就诚实返回实际 mountPath,勿回显未兑现值」);跨 K8S_MOUNT_PATH
|
|
486
|
+
* 变更窗的分歧由 core 侧 fail-closed reopen 兜底,审批不烧错根。
|
|
487
|
+
*/
|
|
351
488
|
async resumeVM(snapshotId, options) {
|
|
352
489
|
if (!this.cfg.s3Snapshot)
|
|
353
490
|
return unsupported("resumeVM");
|
|
491
|
+
// Validate + presign BEFORE provisioning a pod — a traversing/garbage snapshotId should fail fast, not leak a pod.
|
|
354
492
|
const presigned = this.presignSnapshot("GET", snapshotId);
|
|
355
493
|
if (!presigned.ok)
|
|
356
494
|
return presigned;
|
|
@@ -358,11 +496,14 @@ export class RemoteK8sExecutionEnv {
|
|
|
358
496
|
if (!c.ok)
|
|
359
497
|
return c;
|
|
360
498
|
const url = presigned.value;
|
|
499
|
+
// Capture the HTTP status (curl -s -w) and branch on it: 404 → empty/plan-only snapshot, leave the fresh
|
|
500
|
+
// empty workspace; non-200 → real error (exit 22); 200 → untar. (No `-f`: we need the status code, not a
|
|
501
|
+
// curl error exit, to distinguish the tolerated 404 from a genuine failure.)
|
|
361
502
|
const cmd = `code=$(curl -s -o /tmp/ws.tgz -w '%{http_code}' ${shellQuote(url)}); ` +
|
|
362
503
|
`if [ "$code" = "404" ]; then echo EMPTY_SNAPSHOT; exit 0; fi; ` +
|
|
363
504
|
`if [ "$code" != "200" ]; then echo "snapshot GET HTTP $code" >&2; exit 22; fi; ` +
|
|
364
505
|
`tar xzf /tmp/ws.tgz -C ${shellQuote(this.cfg.mountPath)} && rm -f /tmp/ws.tgz`;
|
|
365
|
-
const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 });
|
|
506
|
+
const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs, livenessMs: 0 }); // silent bulk download → wall bound only
|
|
366
507
|
if (!r.ok)
|
|
367
508
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot restore failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
|
|
368
509
|
if (r.value.exitCode !== 0)
|
|
@@ -370,12 +511,19 @@ export class RemoteK8sExecutionEnv {
|
|
|
370
511
|
this.handleSnapshotId = snapshotId;
|
|
371
512
|
return ok(this.workspaceHandle());
|
|
372
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Honest post-resume consistency (design/48 §5 council #8): the workspace FILES are restored byte-for-byte
|
|
516
|
+
* from the snapshot, so read-before-edit hashing already lines up — there's nothing to re-fetch. In-VM
|
|
517
|
+
* process state (a language server) is gone by design; core/the LSP manager re-establishes it lazily. So
|
|
518
|
+
* this is a successful no-op rather than the previous `unsupported` (which forced a cold rebuild).
|
|
519
|
+
*/
|
|
373
520
|
async postResumeInit() {
|
|
374
521
|
if (!this.cfg.s3Snapshot)
|
|
375
522
|
return unsupported("postResumeInit");
|
|
376
523
|
return ok(undefined);
|
|
377
524
|
}
|
|
378
525
|
async reconnect(_sessionToken) {
|
|
526
|
+
// The pod either still runs (reuse) or is gone (fresh connect).
|
|
379
527
|
if (this.podName) {
|
|
380
528
|
const alive = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}`).catch(() => undefined);
|
|
381
529
|
const phase = alive?.json?.status?.phase;
|
|
@@ -389,7 +537,10 @@ export class RemoteK8sExecutionEnv {
|
|
|
389
537
|
async deletePod(name) {
|
|
390
538
|
await this.request("DELETE", `/api/v1/namespaces/${this.cfg.namespace}/pods/${name}?gracePeriodSeconds=0`, undefined, { timeoutMs: this.cfg.rpcTimeoutMs });
|
|
391
539
|
}
|
|
540
|
+
/** Delete the sandbox pod — the workspace is DISPOSABLE (vs SSH/ADB disconnect-only). Idempotent, never throws. */
|
|
392
541
|
async destroy() {
|
|
542
|
+
// design/103 §3.7 backstop: kill/clear any background jobs while the pod is still alive (core's run-loop finally
|
|
543
|
+
// usually disposed already → this iterates an empty registry). After this the pod delete reaps anything left.
|
|
393
544
|
await this.disposeBackgroundShells().catch(() => { });
|
|
394
545
|
this.destroyed = true;
|
|
395
546
|
const name = this.podName;
|
|
@@ -401,6 +552,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
401
552
|
async cleanup() {
|
|
402
553
|
return this.destroy();
|
|
403
554
|
}
|
|
555
|
+
// ─────────────────────────────── exec ───────────────────────────────
|
|
404
556
|
execUrl(command) {
|
|
405
557
|
const { apiUrl, token } = this.creds();
|
|
406
558
|
const base = apiUrl.replace(/^http/, "ws");
|
|
@@ -412,7 +564,18 @@ export class RemoteK8sExecutionEnv {
|
|
|
412
564
|
q.append("command", part);
|
|
413
565
|
return { url: `${base}/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}/exec?${q.toString()}`, headers: { authorization: `Bearer ${token}` } };
|
|
414
566
|
}
|
|
415
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Run a command over the exec WebSocket. Frames: byte0=channel (1 stdout / 2 stderr / 3 status JSON with the
|
|
569
|
+
* REAL exit code). Bounds: caller wall-clock (explicit `timeout`) + zero-progress liveness (`livenessMs`) —
|
|
570
|
+
* a socket killed by either does NOT kill the remote process (disposable-pod posture, see header).
|
|
571
|
+
*/
|
|
572
|
+
runExec(command,
|
|
573
|
+
// 🔴 binary caveat (D2): runExec is SHARED by the model-facing TEXT exec / execStream (boundOutput:true —
|
|
574
|
+
// a `yes`/`cat big` must not OOM the durable worker) AND by readBinaryFile, which reads the file as base64 over
|
|
575
|
+
// execData→runExec and decodes r.value.stdout. Bounding that base64 stream would SILENTLY CORRUPT binary/large
|
|
576
|
+
// reads. So boundOutput DEFAULTS OFF: every data path (execData, snapshot tar up/down, mkdir) keeps EVERY byte
|
|
577
|
+
// byte-exact; ONLY exec()/execStream() opt in. stderr is always bounded (never the file payload).
|
|
578
|
+
opts) {
|
|
416
579
|
let sock;
|
|
417
580
|
let headers;
|
|
418
581
|
let url;
|
|
@@ -424,19 +587,27 @@ export class RemoteK8sExecutionEnv {
|
|
|
424
587
|
return Promise.resolve({ ok: false, error: new ExecutionError("shell_unavailable", e instanceof Error ? e.message : String(e), e instanceof Error ? e : undefined) });
|
|
425
588
|
}
|
|
426
589
|
return new Promise((resolvePromise) => {
|
|
590
|
+
// boundOutput (see signature): TEXT exec/execStream bound stdout to a rolling-TAIL (last 8MB + dropped marker,
|
|
591
|
+
// O(maxBytes)); data paths (boundOutput off) keep full bytes so readBinaryFile's base64 decodes losslessly.
|
|
592
|
+
// stderr is ALWAYS bounded — it never carries the base64 file payload.
|
|
427
593
|
const boundOutput = !!opts?.boundOutput;
|
|
428
|
-
const outBufs = [];
|
|
429
|
-
const outTail = boundOutput ? new RollingTailBuffer() : undefined;
|
|
594
|
+
const outBufs = []; // data path (boundOutput off): full bytes
|
|
595
|
+
const outTail = boundOutput ? new RollingTailBuffer() : undefined; // text path: bounded stdout
|
|
430
596
|
const errBuf = new RollingTailBuffer();
|
|
597
|
+
// Per-channel UTF-8 decoders for the STREAMING callbacks only (council BUG#1): a multibyte codepoint can
|
|
598
|
+
// straddle two WS frames → a per-frame `payload.toString("utf8")` emits U+FFFD. StringDecoder buffers the
|
|
599
|
+
// incomplete tail across writes. (The buffered path decodes once via Buffer.concat below — already safe.)
|
|
431
600
|
const outDecoder = opts?.onStdout ? new StringDecoder("utf8") : undefined;
|
|
432
601
|
const errDecoder = opts?.onStderr ? new StringDecoder("utf8") : undefined;
|
|
433
602
|
let statusFrame;
|
|
434
603
|
let settled = false;
|
|
435
604
|
let lastProgress = Date.now();
|
|
605
|
+
// design/116 detach: once set, the socket's frames belong to the ADOPTED shell — message/error/close route
|
|
606
|
+
// here instead of the (already-settled) foreground buffers, and the socket is NOT closed on settle.
|
|
436
607
|
let spliceSink;
|
|
437
|
-
let sinkClosed = false;
|
|
608
|
+
let sinkClosed = false; // error+close can BOTH fire — notify the adopted shell's terminal exactly once
|
|
438
609
|
let removeDetachListener;
|
|
439
|
-
const detachTee = opts?.detach?.tee;
|
|
610
|
+
const detachTee = opts?.detach?.tee; // fed on every fg frame; a later adoption takes the pair over as-is (review #3)
|
|
440
611
|
const finish = (r) => {
|
|
441
612
|
if (settled)
|
|
442
613
|
return;
|
|
@@ -449,12 +620,19 @@ export class RemoteK8sExecutionEnv {
|
|
|
449
620
|
(sock.terminate ?? sock.close).call(sock);
|
|
450
621
|
}
|
|
451
622
|
catch {
|
|
623
|
+
/* already gone */
|
|
452
624
|
}
|
|
453
625
|
resolvePromise(r);
|
|
454
626
|
};
|
|
455
627
|
const wallMs = opts?.timeoutMs;
|
|
456
628
|
const wallTimer = wallMs ? setTimeout(() => finish({ ok: false, error: new ExecutionError("timeout", `command exceeded ${wallMs}ms`) }), wallMs) : undefined;
|
|
629
|
+
// zero-progress liveness (E2B class-fix semantics): on firing, re-check actual idleness and re-arm for
|
|
630
|
+
// EXACTLY the remaining window (the E2B waitWithLiveness/council-DESIGN-1 discipline — a full-window
|
|
631
|
+
// re-arm would let a hang sit for up to 2× livenessMs before detection).
|
|
457
632
|
let livenessTimer;
|
|
633
|
+
// Per-call liveness override (0 = disabled): bulk data transfers (tar→S3 upload, S3→untar restore) are
|
|
634
|
+
// legitimately SILENT for their whole duration, so the zero-output liveness would false-positive a "hang";
|
|
635
|
+
// they rely on the wall-clock `timeoutMs` (dataTimeoutMs) instead. Defaults to the configured command bound.
|
|
458
636
|
const livenessMs = opts?.livenessMs ?? this.cfg.livenessMs;
|
|
459
637
|
const armLiveness = () => {
|
|
460
638
|
if (livenessMs <= 0)
|
|
@@ -481,8 +659,10 @@ export class RemoteK8sExecutionEnv {
|
|
|
481
659
|
return;
|
|
482
660
|
lastProgress = Date.now();
|
|
483
661
|
const channel = buf[0];
|
|
484
|
-
const payload = buf.subarray(1);
|
|
662
|
+
const payload = buf.subarray(1); // zero-copy view; `ws` gives each message its own buffer (no reuse),
|
|
485
663
|
if (spliceSink) {
|
|
664
|
+
// Adopted: output frames feed the background shell's drain buffers; the status frame still lands in
|
|
665
|
+
// `statusFrame` so the close handler can classify the adopted shell's terminal.
|
|
486
666
|
if (channel === 1 || channel === 2)
|
|
487
667
|
spliceSink.data(channel, payload);
|
|
488
668
|
else if (channel === 3)
|
|
@@ -491,11 +671,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
491
671
|
}
|
|
492
672
|
if (channel === 1) {
|
|
493
673
|
if (boundOutput)
|
|
494
|
-
outTail.push(payload);
|
|
674
|
+
outTail.push(payload); // text path: bounded tail (streaming callback below still gets the full chunk)
|
|
495
675
|
else
|
|
496
|
-
outBufs.push(payload);
|
|
676
|
+
outBufs.push(payload); // data path: full bytes, safe to retain until the final Buffer.concat (council perf#4)
|
|
497
677
|
if (detachTee)
|
|
498
|
-
feedMemStream(detachTee.out, payload, K8S_BG_MEM_CAP);
|
|
678
|
+
feedMemStream(detachTee.out, payload, K8S_BG_MEM_CAP); // tee-from-start (review #3): one decoder spans the detach instant
|
|
499
679
|
if (outDecoder)
|
|
500
680
|
opts.onStdout(outDecoder.write(payload));
|
|
501
681
|
}
|
|
@@ -513,6 +693,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
513
693
|
sock.on("error", (...args) => {
|
|
514
694
|
const e = args[0];
|
|
515
695
|
if (spliceSink) {
|
|
696
|
+
// Adopted: a socket error mid-background = the output feed is lost; outcome unknown → failed. The
|
|
697
|
+
// process itself may still run in the pod (disposable-pod posture — teardown reaps it).
|
|
516
698
|
if (!sinkClosed) {
|
|
517
699
|
sinkClosed = true;
|
|
518
700
|
spliceSink.closed({ failed: true });
|
|
@@ -526,12 +708,16 @@ export class RemoteK8sExecutionEnv {
|
|
|
526
708
|
if (sinkClosed)
|
|
527
709
|
return;
|
|
528
710
|
sinkClosed = true;
|
|
711
|
+
// Terminal classification mirrors the launch-mode poll: a status frame with an exit code ⇒ exited
|
|
712
|
+
// (non-zero exit is a NORMAL result); no status ⇒ the transport died mid-background ⇒ failed.
|
|
529
713
|
if (statusFrame === undefined)
|
|
530
714
|
return spliceSink.closed({ failed: true });
|
|
531
715
|
const parsed = exitCodeFromStatus(statusFrame);
|
|
532
716
|
return spliceSink.closed("error" in parsed ? { failed: true } : { failed: false, exitCode: parsed.exitCode });
|
|
533
717
|
}
|
|
534
718
|
if (statusFrame === undefined) {
|
|
719
|
+
// closed without a status frame = the transport died mid-command, NOT a command result ([R78]#1).
|
|
720
|
+
// Typed "transport_lost" since core 1.89 (outcome UNKNOWN — retry only per the op's idempotency).
|
|
535
721
|
finish({ ok: false, error: new ExecutionError("transport_lost", "exec socket closed without status — k8s transport lost mid-command") });
|
|
536
722
|
return;
|
|
537
723
|
}
|
|
@@ -547,21 +733,32 @@ export class RemoteK8sExecutionEnv {
|
|
|
547
733
|
finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode: parsed.exitCode }));
|
|
548
734
|
}
|
|
549
735
|
else {
|
|
736
|
+
// byte-exact: readBinaryFile decodes this stdout as base64; never truncate.
|
|
550
737
|
finish(ok({ stdout: Buffer.concat(outBufs).toString("utf8"), stderr, exitCode: parsed.exitCode }));
|
|
551
738
|
}
|
|
552
739
|
});
|
|
740
|
+
// design/116 detach — wired AFTER every handler above exists (the host adapter's create-then-abort TDZ
|
|
741
|
+
// lesson): core's ToolDetachHub can hand an ALREADY-aborted signal (detach requested before the exec
|
|
742
|
+
// started), in which case onDetach runs synchronously right here.
|
|
553
743
|
const detach = opts?.detach;
|
|
554
744
|
if (detach) {
|
|
555
745
|
const onDetach = () => {
|
|
556
746
|
if (settled || spliceSink)
|
|
557
747
|
return;
|
|
558
|
-
const out = outTail ? outTail.result() : { text: "", droppedBytes: 0 };
|
|
748
|
+
const out = outTail ? outTail.result() : { text: "", droppedBytes: 0 }; // detach rides the exec path (boundOutput:true)
|
|
559
749
|
const err = errBuf.result();
|
|
560
750
|
const shellId = detach.adopt({ sock, reroute: (sink) => { spliceSink = sink; } });
|
|
561
751
|
if (shellId === undefined) {
|
|
752
|
+
// Refused (limit / builder failure) → the foreground continues untouched. 🔴 review #5: `reroute` is a
|
|
753
|
+
// builder SIDE EFFECT — if the builder ran it and then threw, adoptSync's catch returns undefined but
|
|
754
|
+
// spliceSink is already set, which would silently reroute every subsequent fg frame into an unregistered
|
|
755
|
+
// buffer AND make this exec never settle (close would notify a sink nobody owns). Un-splice defensively;
|
|
756
|
+
// same-tick, so no frame can have landed in between.
|
|
562
757
|
spliceSink = undefined;
|
|
563
758
|
return;
|
|
564
759
|
}
|
|
760
|
+
// Settle EARLY *without* closing the socket — the adopted shell owns it now. finish() is deliberately
|
|
761
|
+
// NOT used (it terminates the socket); this is its detach twin.
|
|
565
762
|
settled = true;
|
|
566
763
|
clearTimeout(wallTimer);
|
|
567
764
|
clearTimeout(livenessTimer);
|
|
@@ -582,21 +779,29 @@ export class RemoteK8sExecutionEnv {
|
|
|
582
779
|
const c = await this.ensureConnected();
|
|
583
780
|
if (!c.ok)
|
|
584
781
|
return c;
|
|
782
|
+
// design/116 detach (core 1.207, 1.82): the Bash tool threads a `detachSignal` for every exec once
|
|
783
|
+
// `supportsDetach` is advertised. `ExecOpts` doesn't type it yet (core-side seam) — read it structurally.
|
|
585
784
|
const detachSignal = options?.detachSignal;
|
|
586
|
-
this.inFlightExecs++;
|
|
785
|
+
this.inFlightExecs++; // suspendVM refuses while > 0 (council #4: never snapshot a running command)
|
|
587
786
|
try {
|
|
588
787
|
const composed = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
589
788
|
if (!detachSignal) {
|
|
590
789
|
return await this.runExec(composed, {
|
|
591
790
|
signal: options?.abortSignal,
|
|
592
791
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
|
|
593
|
-
boundOutput: true,
|
|
792
|
+
boundOutput: true, // model-facing TEXT exec — bound the buffered return value (D2)
|
|
594
793
|
onStdout: options?.onStdout,
|
|
595
794
|
onStderr: options?.onStderr,
|
|
596
795
|
});
|
|
597
796
|
}
|
|
797
|
+
// Detach-capable exec: the pgid prelude (buildDetachCapableExec) makes the running command adoptable —
|
|
798
|
+
// kill/dispose on the adopted shell reuse the SAME pod-side scripts as a spawned one. On detach, adoptSync
|
|
799
|
+
// splices the still-open exec socket into memory-mode drain buffers (SYNCHRONOUS — no await between the
|
|
800
|
+
// tee handover and the reroute, the host adapter's double-review contract).
|
|
598
801
|
const adoptId = randomUUID();
|
|
599
802
|
const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
|
|
803
|
+
// tee-from-start (Fable-5 review #3): runExec feeds every fg frame into this pair through its per-stream
|
|
804
|
+
// StringDecoder; adoption takes the pair over as-is, so ONE decoder spans the detach instant (no U+FFFD seam).
|
|
600
805
|
const tee = { out: seedMemStream(""), err: seedMemStream("") };
|
|
601
806
|
return await this.runExec(buildDetachCapableExec(dir, composed), {
|
|
602
807
|
signal: options?.abortSignal,
|
|
@@ -608,8 +813,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
608
813
|
signal: detachSignal,
|
|
609
814
|
tee,
|
|
610
815
|
adopt: (io) => {
|
|
816
|
+
// 🔴 builder contract (review #5): everything inside is NON-THROWING (object construction + the reroute
|
|
817
|
+
// assignment). adoptSync's catch does NOT undo side effects — future logic that can throw must go before
|
|
818
|
+
// `io.reroute` (the runExec caller also un-splices defensively on an undefined return).
|
|
611
819
|
const adopted = this.bgManager.adoptSync((ctx) => {
|
|
612
|
-
const mem = { out: tee.out, err: tee.err };
|
|
820
|
+
const mem = { out: tee.out, err: tee.err }; // the tee already holds the full fg history (truncation marker rides on the exec's OWN early result only — review LOW)
|
|
613
821
|
io.reroute({
|
|
614
822
|
data: (channel, payload) => feedMemStream(channel === 1 ? mem.out : mem.err, payload, K8S_BG_MEM_CAP),
|
|
615
823
|
closed: (terminal) => ctx.onTerminal(terminal.failed, terminal.exitCode),
|
|
@@ -625,13 +833,28 @@ export class RemoteK8sExecutionEnv {
|
|
|
625
833
|
this.inFlightExecs--;
|
|
626
834
|
}
|
|
627
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* LSP surface (lsp/manager.ts `isLspCapable` duck-type, design/64 §13.1): cluster-internal address for a
|
|
838
|
+
* port on the sandbox pod. The E2B counterpart returns a public proxy host; a pod has none — the WORKER
|
|
839
|
+
* must be able to reach pod IPs (in-cluster deployment, or on-node), and the transport is plain `ws://`
|
|
840
|
+
* (cluster network, no TLS — the per-session bridge token still gates access like on E2B).
|
|
841
|
+
*/
|
|
628
842
|
async getHost(port) {
|
|
629
843
|
const c = await this.ensureConnected();
|
|
630
844
|
if (!c.ok || !this.podIP)
|
|
631
|
-
return undefined;
|
|
845
|
+
return undefined; // no IP captured (e.g. faked/older apiserver) → LSP degrades
|
|
632
846
|
return `${this.podIP}:${port}`;
|
|
633
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Start a long-lived helper (the LSP bridge) detached from this exec session. Unlike E2B — which reaps a
|
|
850
|
+
* foreground command's process group on completion, forcing a native background API — k8s exec just closes
|
|
851
|
+
* the session: a `nohup`'d child with detached stdio survives, reparented to the container's PID 1 (the
|
|
852
|
+
* gate#2 background-gradle runs proved this through the production path). Output goes to /tmp for postmortem.
|
|
853
|
+
*/
|
|
634
854
|
async startBackground(command) {
|
|
855
|
+
// `command` is a COMPOUND shell line (lspStartCommand: `cd … && ENV=… node bridge.cjs`) — nohup takes a
|
|
856
|
+
// single executable, so wrap the whole line in `sh -c`. Caught live: `nohup cd …` silently started nothing
|
|
857
|
+
// and the bridge connect timed out (gate#2 k8s-LSP attempt 1).
|
|
635
858
|
const wrapped = command.replace(/[\\"$`]/g, (c) => "\\" + c);
|
|
636
859
|
const r = await this.exec(`(nohup sh -c "${wrapped}" >>/tmp/lsp-bridge.log 2>&1 &) ; echo started`, { timeout: 30 });
|
|
637
860
|
if (!r.ok)
|
|
@@ -643,7 +866,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
643
866
|
const c = await this.ensureConnected();
|
|
644
867
|
if (!c.ok)
|
|
645
868
|
throw new RemoteExecutionError("connect_failed", c.error.message, c.error);
|
|
646
|
-
this.inFlightExecs++;
|
|
869
|
+
this.inFlightExecs++; // suspendVM refuses while > 0 (council #4); decremented in the finally below
|
|
647
870
|
const queue = [];
|
|
648
871
|
let finished = false;
|
|
649
872
|
let failure;
|
|
@@ -656,12 +879,21 @@ export class RemoteK8sExecutionEnv {
|
|
|
656
879
|
};
|
|
657
880
|
let lastChunk = Date.now();
|
|
658
881
|
const idleMs = options?.readTimeoutMs ?? this.cfg.livenessMs;
|
|
882
|
+
// Total-output cap (D2): parity with the host/local-docker execStream — when the caller sets
|
|
883
|
+
// maxOutputBytes, count yielded bytes and abort+throw past the cap (the finally's ac.abort() then closes
|
|
884
|
+
// the exec socket). Previously k8s execStream IGNORED maxOutputBytes (it was the only adapter that did).
|
|
659
885
|
const cap = options?.maxOutputBytes;
|
|
660
886
|
let bytes = 0;
|
|
887
|
+
// Internal abort composed with the caller's: EVERY generator exit (normal end, idle-throw, consumer
|
|
888
|
+
// early-break via .return()) must settle runExec IMMEDIATELY — without this, the finally's `await done`
|
|
889
|
+
// would sit until runExec's own liveness fired (~minutes) with the exec socket held open (self-review A).
|
|
661
890
|
const ac = new AbortController();
|
|
662
891
|
const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
|
|
663
892
|
const done = this.runExec(this.withCwdEnv(command, options?.cwd, options?.env), {
|
|
664
893
|
signal,
|
|
894
|
+
// The consumer reads from the onStdout/onStderr callbacks (full chunks); runExec's retained buffer is
|
|
895
|
+
// discarded here (only exitCode is used). boundOutput:true caps that otherwise-unbounded retained buffer
|
|
896
|
+
// with zero change to what the stream consumer sees (D2).
|
|
665
897
|
boundOutput: true,
|
|
666
898
|
onStdout: (d) => {
|
|
667
899
|
lastChunk = Date.now();
|
|
@@ -677,9 +909,12 @@ export class RemoteK8sExecutionEnv {
|
|
|
677
909
|
if (r.ok)
|
|
678
910
|
exitCode = r.value.exitCode;
|
|
679
911
|
else {
|
|
912
|
+
// Map the buffered-exec ExecutionErrorCode → a RemoteExecutionErrorCode. `spawn_error`/`shell_unavailable`
|
|
913
|
+
// mean the environment is fundamentally broken → "unknown" (non-retryable), NOT "connect_failed" (which
|
|
914
|
+
// the retry whitelist would re-attempt against a broken shell — council BUG#2).
|
|
680
915
|
const code = r.error.code === "aborted"
|
|
681
916
|
? "aborted"
|
|
682
|
-
: r.error.code === "transport_lost"
|
|
917
|
+
: r.error.code === "transport_lost" // typed end-to-end since core 1.89 (was a message marker)
|
|
683
918
|
? "transport_lost"
|
|
684
919
|
: r.error.code === "timeout"
|
|
685
920
|
? "timeout"
|
|
@@ -720,11 +955,23 @@ export class RemoteK8sExecutionEnv {
|
|
|
720
955
|
yield { type: "exit", exitCode };
|
|
721
956
|
}
|
|
722
957
|
finally {
|
|
723
|
-
ac.abort();
|
|
958
|
+
ac.abort(); // settle runExec now (no-op if already finished) — closes the socket, frees the timers
|
|
724
959
|
await done.catch(() => { });
|
|
725
960
|
this.inFlightExecs--;
|
|
726
961
|
}
|
|
727
962
|
}
|
|
963
|
+
// ─────────────────────────── design/103 background shell (TOB, path ①) ───────────────────────────
|
|
964
|
+
//
|
|
965
|
+
// kata/k8s takes path ① (design/103 §5.2): the pod filesystem buffers output, re-read by byte cursor each poll
|
|
966
|
+
// — unlike E2B there is no provider job handle, and closing the exec socket does NOT kill the remote process
|
|
967
|
+
// (disposable-pod posture, see file header), so termination is detected by an exit-sentinel file and kill goes
|
|
968
|
+
// through the process GROUP (`setsid` + self-reported pgid). Background ops run as short ephemeral execs and
|
|
969
|
+
// deliberately do NOT touch `inFlightExecs` (they are control-plane management, not a foreground command to
|
|
970
|
+
// snapshot; the detached job is killed before `suspendVM` by the core dispose saga, design/103 §3.7).
|
|
971
|
+
/**
|
|
972
|
+
* A background job cannot outlive its pod (`activeDeadlineSeconds` ≈ `timeoutMs`), so the BG ceiling is clamped to
|
|
973
|
+
* it (assigned in the constructor — `this.cfg` is set there, so a field initializer would read it pre-init).
|
|
974
|
+
*/
|
|
728
975
|
backgroundCapabilities;
|
|
729
976
|
_bgManager;
|
|
730
977
|
get bgManager() {
|
|
@@ -737,9 +984,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
737
984
|
async launch(command, options, ctx) {
|
|
738
985
|
const conn = await self.ensureConnected();
|
|
739
986
|
if (!conn.ok)
|
|
740
|
-
throw new Error(`pod connect failed: ${conn.error.message}`);
|
|
987
|
+
throw new Error(`pod connect failed: ${conn.error.message}`); // → manager wraps as spawn_failed
|
|
741
988
|
const jobId = randomUUID();
|
|
742
989
|
const dir = `${K8S_BG_DIR_ROOT}/${jobId}`;
|
|
990
|
+
// 🔴 secret-scrub red line (design/103 §3.1): cwd + env injected via the SAME `withCwdEnv` path as exec —
|
|
991
|
+
// the host process.env is never injected; only the scrubbed task env core passes reaches the child.
|
|
743
992
|
const inner = self.withCwdEnv(command, options?.cwd, options?.env);
|
|
744
993
|
const cmdB64 = Buffer.from(inner, "utf8").toString("base64");
|
|
745
994
|
const runnerB64 = Buffer.from(buildBgRunnerScript(dir), "utf8").toString("base64");
|
|
@@ -749,10 +998,12 @@ export class RemoteK8sExecutionEnv {
|
|
|
749
998
|
if (r.value.exitCode !== 0 || !r.value.stdout.includes("SEMABG_LAUNCHED")) {
|
|
750
999
|
throw new Error(`background launch exited ${r.value.exitCode}: ${r.value.stderr.slice(0, 200)}`);
|
|
751
1000
|
}
|
|
752
|
-
void ctx;
|
|
1001
|
+
void ctx; // k8s is pull-model: termination is discovered in read(); ctx.onTerminal is intentionally unused
|
|
753
1002
|
return { jobId, dir, stdoutCursor: 0, stderrCursor: 0 };
|
|
754
1003
|
},
|
|
755
1004
|
async read(state) {
|
|
1005
|
+
// ADOPT (memory) mode: the still-open exec socket feeds the drain buffers; termination is PUSH-model
|
|
1006
|
+
// (socket close → ctx.onTerminal), so `read` never reports terminal here — mirror of the E2B/host adopt.
|
|
756
1007
|
if (state.adopt) {
|
|
757
1008
|
const o = drainMemStream(state.adopt.mem.out);
|
|
758
1009
|
const e = drainMemStream(state.adopt.mem.err);
|
|
@@ -770,15 +1021,20 @@ export class RemoteK8sExecutionEnv {
|
|
|
770
1021
|
if (!p)
|
|
771
1022
|
throw new Error("background poll: unparseable response");
|
|
772
1023
|
if (p.gone) {
|
|
1024
|
+
// dir vanished while still owned (dispose clears the registry FIRST, so this isn't us) → abnormal end.
|
|
773
1025
|
return { stdout: "", stderr: "", bytesFromStart: state.stdoutCursor + state.stderrCursor, bytesDroppedBeforeCursor: 0, terminal: { failed: true } };
|
|
774
1026
|
}
|
|
775
1027
|
const droppedO = Math.max(0, p.outTotal - state.stdoutCursor - p.stdoutBytes);
|
|
776
1028
|
const droppedE = Math.max(0, p.errTotal - state.stderrCursor - p.stderrBytes);
|
|
777
1029
|
state.stdoutCursor = p.outTotal;
|
|
778
1030
|
state.stderrCursor = p.errTotal;
|
|
1031
|
+
// Terminal: exit sentinel present → exited (exit code known); else process group gone without a sentinel →
|
|
1032
|
+
// failed (abnormal — a clean exit always writes the sentinel as the runner's last act before the group dies).
|
|
779
1033
|
let terminal = p.exitCode !== undefined ? { failed: false, exitCode: p.exitCode } : !p.alive ? { failed: true } : undefined;
|
|
1034
|
+
// 🔴 disk guard: direct redirect has no in-pod cap, so reap a runaway whose output exceeds K8S_BG_FILE_CAP
|
|
1035
|
+
// (the kata VM isolates the blast to this disposable pod; the BG-timeout + activeDeadlineSeconds also bound it).
|
|
780
1036
|
if (!terminal && (p.outTotal > K8S_BG_FILE_CAP || p.errTotal > K8S_BG_FILE_CAP)) {
|
|
781
|
-
void bgExec(buildBgKillScript(state.dir), true).catch(() => { });
|
|
1037
|
+
void bgExec(buildBgKillScript(state.dir), true).catch(() => { }); // fire-and-forget reap
|
|
782
1038
|
terminal = { failed: true };
|
|
783
1039
|
}
|
|
784
1040
|
return {
|
|
@@ -790,21 +1046,23 @@ export class RemoteK8sExecutionEnv {
|
|
|
790
1046
|
};
|
|
791
1047
|
},
|
|
792
1048
|
async kill(state) {
|
|
793
|
-
await bgExec(buildBgKillScript(state.dir), true).catch(() => { });
|
|
1049
|
+
await bgExec(buildBgKillScript(state.dir), true).catch(() => { }); // best-effort, idempotent
|
|
794
1050
|
},
|
|
795
1051
|
async disposeOne(state) {
|
|
1052
|
+
// Adopted shells hold the (kept-open) exec socket — release it on EVERY dispose path, even when the pod
|
|
1053
|
+
// is already being torn down (the socket is a worker-side resource, not a pod-side one).
|
|
796
1054
|
const closeAdopted = () => {
|
|
797
1055
|
if (!state.adopt)
|
|
798
1056
|
return;
|
|
799
1057
|
try {
|
|
800
1058
|
(state.adopt.sock.terminate ?? state.adopt.sock.close).call(state.adopt.sock);
|
|
801
1059
|
}
|
|
802
|
-
catch { }
|
|
1060
|
+
catch { /* already gone */ }
|
|
803
1061
|
};
|
|
804
1062
|
if (self.destroyed) {
|
|
805
1063
|
closeAdopted();
|
|
806
1064
|
return;
|
|
807
|
-
}
|
|
1065
|
+
} // pod teardown reaps the job; don't resurrect a connection
|
|
808
1066
|
await bgExec(buildBgDisposeScript(state.dir), true).catch(() => { });
|
|
809
1067
|
closeAdopted();
|
|
810
1068
|
},
|
|
@@ -820,8 +1078,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
820
1078
|
return this.bgManager.kill(shellId);
|
|
821
1079
|
}
|
|
822
1080
|
disposeBackgroundShells(opts) {
|
|
1081
|
+
// 签名统一带 opts(core 尾调 {except: keepAlive})。隔离 env 的留活是名义的——沙箱拆机进程必然随之
|
|
1082
|
+
// 消亡(destroy 整体带走),core 的 envDying settle 会先给准确 killed 回执;透传保证签名不静默丢参。
|
|
823
1083
|
return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
|
|
824
1084
|
}
|
|
1085
|
+
// ─────────────────────────────── FileSystem (over exec; real exit codes) ───────────────────────────────
|
|
825
1086
|
async absolutePath(p) {
|
|
826
1087
|
return ok(this.resolve(p));
|
|
827
1088
|
}
|
|
@@ -830,9 +1091,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
830
1091
|
}
|
|
831
1092
|
async readBinaryFile(p, abortSignal) {
|
|
832
1093
|
const abs = this.resolve(p);
|
|
1094
|
+
// base64 keeps the WS text-safe; the REAL exit code separates "empty file" from "read failed".
|
|
833
1095
|
const r = await this.execData(`base64 < ${shellQuote(abs)}`, abortSignal);
|
|
834
1096
|
if (!r.ok)
|
|
835
1097
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
1098
|
+
// Non-zero base64 read → central classifier, not_found fallback (truncated-stderr tolerant — council BUG#3).
|
|
836
1099
|
if (r.value.exitCode !== 0)
|
|
837
1100
|
return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
|
|
838
1101
|
return ok(new Uint8Array(Buffer.from(r.value.stdout.replace(/\s+/g, ""), "base64")));
|
|
@@ -852,6 +1115,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
852
1115
|
lines = lines.slice(0, options.maxLines);
|
|
853
1116
|
return ok(lines);
|
|
854
1117
|
}
|
|
1118
|
+
/** 父目录自建(与 host/local-docker/adb/ssh 同口径)。抽成共用是因为 `appendFile` 曾**绕过**它:
|
|
1119
|
+
* 🔴 2026-07-25 实测的跨腿分叉 —— `appendFile` 直接调 `writeChunked`,整条路上没有 `mkdir -p`,于是
|
|
1120
|
+
* 「直接 append 建一个新日志路径」在 k8s 腿上返 `not_found`,而其余 5 腿都成功(host/local-docker/adb 实测,
|
|
1121
|
+
* ssh/e2b 由方法体判据)。core 的后台输出镜像正是"每拍 append 一次"的形,这条分叉会让它在 k8s 腿上从第一拍
|
|
1122
|
+
* 就失败,而 `mirrorFailed` 只会表现成一句「output file is INCOMPLETE」。 */
|
|
855
1123
|
async ensureParentDir(abs, abortSignal) {
|
|
856
1124
|
const dir = path.posix.dirname(abs);
|
|
857
1125
|
if (!dir || dir === "." || dir === "/")
|
|
@@ -868,15 +1136,21 @@ export class RemoteK8sExecutionEnv {
|
|
|
868
1136
|
const bad = await this.ensureParentDir(abs, abortSignal);
|
|
869
1137
|
if (bad)
|
|
870
1138
|
return bad;
|
|
871
|
-
return this.writeChunked(abs, content, true, abortSignal);
|
|
1139
|
+
return this.writeChunked(abs, content, true, abortSignal); // truncate first
|
|
872
1140
|
}
|
|
873
1141
|
async appendFile(p, content, abortSignal) {
|
|
874
1142
|
const abs = this.resolve(p);
|
|
875
|
-
const bad = await this.ensureParentDir(abs, abortSignal);
|
|
1143
|
+
const bad = await this.ensureParentDir(abs, abortSignal); // 见 ensureParentDir 旁注:此前这条路漏了它
|
|
876
1144
|
if (bad)
|
|
877
1145
|
return bad;
|
|
878
|
-
return this.writeChunked(abs, content, false, abortSignal);
|
|
1146
|
+
return this.writeChunked(abs, content, false, abortSignal); // append from byte 0
|
|
879
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Write `content` to `abs` via argv-embedded base64, chunked at WRITE_CHUNK_BYTES (no stdin on the exec
|
|
1150
|
+
* channel we use; v4 has no per-channel close). `truncateFirst` → the FIRST chunk uses `>` (overwrite),
|
|
1151
|
+
* the rest `>>` (append); `false` → all chunks `>>`. Empty content still runs exactly one command so an
|
|
1152
|
+
* empty file is created (write) / left intact (append). Shared by writeFile/appendFile (council DESIGN#2).
|
|
1153
|
+
*/
|
|
880
1154
|
async writeChunked(abs, content, truncateFirst, abortSignal) {
|
|
881
1155
|
const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
882
1156
|
for (let off = 0, first = true; off < bytes.length || first; off += WRITE_CHUNK_BYTES, first = false) {
|
|
@@ -890,6 +1164,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
890
1164
|
}
|
|
891
1165
|
return ok(undefined);
|
|
892
1166
|
}
|
|
1167
|
+
// ── design/158 B6:POSIX shell-out 文件面全部委托共享基座(posix-shell-fs.ts)─────────────────────
|
|
1168
|
+
// 六裁决(`--`/classifyFsStderr/ls -1A/mv 后缀形/temp 位置参数化/`[ -e ]||[ -L ]` exists)与
|
|
1169
|
+
// readLink・lstat 契约见基座头注;本腿此前的 fileErrorFrom 分类形与 exists 形即基座正形的出处。
|
|
893
1170
|
posixFsInst;
|
|
894
1171
|
get posixFs() {
|
|
895
1172
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
@@ -925,6 +1202,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
925
1202
|
async createTempFile(options) {
|
|
926
1203
|
return this.posixFs.createTempFile(options);
|
|
927
1204
|
}
|
|
1205
|
+
// ─────────────────────────────── helpers ───────────────────────────────
|
|
1206
|
+
/** exec with the data-transfer bound (large reads/writes legitimately run longer than a control op). */
|
|
928
1207
|
async execData(command, abortSignal) {
|
|
929
1208
|
const c = await this.ensureConnected();
|
|
930
1209
|
if (!c.ok)
|
|
@@ -943,7 +1222,14 @@ export class RemoteK8sExecutionEnv {
|
|
|
943
1222
|
return parts.join(" ");
|
|
944
1223
|
}
|
|
945
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* `ExecutionEnvFactory` for the k8s pod-sandbox backend — one fresh (unconnected) env per task; the pod is
|
|
1227
|
+
* created lazily on connect and deleted by `destroy()` (Runner-owned lifetime; `activeDeadlineSeconds` is the
|
|
1228
|
+
* leak guard if destroy never runs).
|
|
1229
|
+
*/
|
|
946
1230
|
export function k8sExecutionEnvFactory(config) {
|
|
1231
|
+
// Thread the task's sessionId so workspace-snapshot keys are namespaced per session
|
|
1232
|
+
// (workspaces/<prefix>/<sessionId>/<snapshotId>.tar.gz).
|
|
947
1233
|
return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId });
|
|
948
1234
|
}
|
|
949
1235
|
//# sourceMappingURL=remote-env-k8s.js.map
|