@sema-agent/server 7.140.0 → 7.141.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.
Files changed (200) hide show
  1. package/MIGRATION.md +290 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +75 -13
  5. package/dist/approval-batch-census.d.ts +27 -0
  6. package/dist/approval-batch-census.js +27 -0
  7. package/dist/approval-card.d.ts +31 -16
  8. package/dist/approval-card.js +6 -3
  9. package/dist/approval.d.ts +15 -0
  10. package/dist/approval.js +10 -0
  11. package/dist/audit.d.ts +4 -2
  12. package/dist/audit.js +2 -2
  13. package/dist/boot/device-lane.d.ts +11 -0
  14. package/dist/boot/device-lane.js +16 -0
  15. package/dist/boot/engine-lease.d.ts +24 -3
  16. package/dist/boot/engine-lease.js +9 -2
  17. package/dist/boot/execution-env.js +8 -4
  18. package/dist/boot/memory-boundary.d.ts +2 -2
  19. package/dist/boot/memory-consolidation.d.ts +1 -1
  20. package/dist/boot/memory-consolidation.js +1 -1
  21. package/dist/boot/reapers.d.ts +2 -2
  22. package/dist/boot/resolve-spec.js +7 -4
  23. package/dist/boot/session-faces.d.ts +1 -1
  24. package/dist/boot/session-faces.js +4 -3
  25. package/dist/boot/shutdown.js +5 -0
  26. package/dist/boot/stage-05-execution-env.js +3 -1
  27. package/dist/boot/stage-06-runners.d.ts +0 -2
  28. package/dist/boot/stage-06-runners.js +1 -1
  29. package/dist/boot/stage-07-capability-layer.d.ts +0 -1
  30. package/dist/boot/stage-07-capability-layer.js +8 -10
  31. package/dist/boot/stage-08-reapers.d.ts +0 -1
  32. package/dist/boot/stage-09-leader.d.ts +0 -1
  33. package/dist/boot/stage-10-http-server.d.ts +0 -1
  34. package/dist/boot/stage-10-http-server.js +1 -0
  35. package/dist/boot/stores.js +10 -4
  36. package/dist/boot/web-admin.js +1 -0
  37. package/dist/capabilities/scenarios.d.ts +1 -1
  38. package/dist/capability-facts.d.ts +9 -0
  39. package/dist/capability-facts.js +9 -0
  40. package/dist/card-void.d.ts +38 -0
  41. package/dist/card-void.js +23 -0
  42. package/dist/config-catalog.js +11 -8
  43. package/dist/config-provider.d.ts +13 -13
  44. package/dist/config-provider.js +19 -33
  45. package/dist/config-types.d.ts +26 -8
  46. package/dist/config.js +55 -46
  47. package/dist/delegated-session-owner.d.ts +21 -19
  48. package/dist/delegated-session-owner.js +1 -1
  49. package/dist/device-store.d.ts +7 -0
  50. package/dist/device-store.js +3 -0
  51. package/dist/execution-config-root.d.ts +22 -0
  52. package/dist/execution-config-root.js +35 -0
  53. package/dist/execution-lane-caps.d.ts +49 -2
  54. package/dist/execution-lane-caps.js +90 -13
  55. package/dist/execution-placement.d.ts +3 -0
  56. package/dist/execution-placement.js +7 -0
  57. package/dist/fleet/drain-entry.d.ts +3 -0
  58. package/dist/hosted-posture.d.ts +8 -0
  59. package/dist/hosted-posture.js +4 -0
  60. package/dist/http/admission.js +3 -3
  61. package/dist/http/caller-trust.d.ts +3 -2
  62. package/dist/http/caller-trust.js +2 -1
  63. package/dist/http/dispatch.js +4 -0
  64. package/dist/http/live-frame.d.ts +24 -0
  65. package/dist/http/live-frame.js +16 -0
  66. package/dist/http/resume-legs.js +37 -20
  67. package/dist/http/route-table.js +2 -0
  68. package/dist/http/routes/approvals-assistant.js +1 -0
  69. package/dist/http/routes/capabilities.js +5 -4
  70. package/dist/http/routes/diagnostics.d.ts +0 -19
  71. package/dist/http/routes/diagnostics.js +16 -2
  72. package/dist/http/routes/memory-bundle.d.ts +2 -2
  73. package/dist/http/routes/memory-bundle.js +1 -1
  74. package/dist/http/routes/memory-compliance.js +1 -1
  75. package/dist/http/routes/memory-policy.js +2 -2
  76. package/dist/http/routes/run-permission-mode.d.ts +75 -0
  77. package/dist/http/routes/run-permission-mode.js +170 -0
  78. package/dist/http/routes/session-sync.d.ts +3 -6
  79. package/dist/http/routes/session-sync.js +7 -2
  80. package/dist/http/routes/sessions.js +5 -4
  81. package/dist/http/routes/tasks.js +63 -66
  82. package/dist/http/run-read-face.js +3 -3
  83. package/dist/http/send.d.ts +1 -1
  84. package/dist/http/send.js +2 -2
  85. package/dist/http/server.d.ts +9 -2
  86. package/dist/http/sse-log.d.ts +3 -8
  87. package/dist/http/stored-body-replay.d.ts +3 -3
  88. package/dist/http/stored-body-replay.js +3 -1
  89. package/dist/instruction-source-notice.d.ts +7 -0
  90. package/dist/instruction-source-notice.js +6 -6
  91. package/dist/mcp-assertion-header.d.ts +4 -0
  92. package/dist/mcp-assertion-header.js +5 -0
  93. package/dist/memory-posture.d.ts +1 -1
  94. package/dist/memory-scope.d.ts +4 -4
  95. package/dist/memory-scope.js +1 -1
  96. package/dist/memory-sync-client.d.ts +14 -2
  97. package/dist/memory-sync-client.js +80 -18
  98. package/dist/observability/fail-open.d.ts +30 -2
  99. package/dist/observability/fail-open.js +30 -2
  100. package/dist/observability/metrics.js +0 -1
  101. package/dist/observability/run-terminal.d.ts +6 -2
  102. package/dist/observability/run-terminal.js +25 -26
  103. package/dist/observability/secret-env-scrub.d.ts +20 -1
  104. package/dist/observability/secret-env-scrub.js +14 -0
  105. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
  106. package/dist/orchestration/workflow-completion-inbox.js +13 -5
  107. package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
  108. package/dist/orchestration/workflow-notify-journal.js +2 -1
  109. package/dist/permission-mode-admission.d.ts +68 -0
  110. package/dist/permission-mode-admission.js +23 -0
  111. package/dist/permission-rule-list.d.ts +55 -6
  112. package/dist/permission-rule-list.js +52 -5
  113. package/dist/plugins/caching-session-store.d.ts +6 -0
  114. package/dist/plugins/caching-session-store.js +3 -0
  115. package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
  116. package/dist/plugins/checkpoint-store-sql.js +74 -3
  117. package/dist/plugins/device-store-sql.d.ts +5 -2
  118. package/dist/plugins/device-store-sql.js +4 -1
  119. package/dist/plugins/execution-placement-sql.d.ts +11 -1
  120. package/dist/plugins/execution-placement-sql.js +12 -1
  121. package/dist/plugins/file-run-store.d.ts +2 -2
  122. package/dist/plugins/file-run-store.js +1 -1
  123. package/dist/plugins/fork-routing-session-store.d.ts +10 -32
  124. package/dist/plugins/fork-routing-session-store.js +2 -61
  125. package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
  126. package/dist/plugins/k8s-exec-protocol.js +6 -1
  127. package/dist/plugins/local-checkpoint-store.d.ts +10 -1
  128. package/dist/plugins/local-checkpoint-store.js +22 -0
  129. package/dist/plugins/local-session-store.d.ts +227 -35
  130. package/dist/plugins/local-session-store.js +518 -37
  131. package/dist/plugins/local-session-window.d.ts +17 -3
  132. package/dist/plugins/local-session-window.js +7 -13
  133. package/dist/plugins/memory-run-store.d.ts +2 -2
  134. package/dist/plugins/memory-run-store.js +1 -1
  135. package/dist/plugins/one-time-migrations.d.ts +42 -2
  136. package/dist/plugins/one-time-migrations.js +65 -3
  137. package/dist/plugins/pg-pool.js +3 -2
  138. package/dist/plugins/pg-session-storage.d.ts +2 -0
  139. package/dist/plugins/pg-session-storage.js +3 -2
  140. package/dist/plugins/remote-env-e2b.d.ts +24 -3
  141. package/dist/plugins/remote-env-e2b.js +131 -18
  142. package/dist/plugins/remote-env-k8s.d.ts +20 -1
  143. package/dist/plugins/remote-env-k8s.js +148 -25
  144. package/dist/plugins/remote-env-local-docker.d.ts +25 -11
  145. package/dist/plugins/remote-env-local-docker.js +87 -21
  146. package/dist/plugins/run-store-sql.d.ts +3 -3
  147. package/dist/plugins/run-store-sql.js +4 -3
  148. package/dist/plugins/sandbox-read-floor.d.ts +273 -0
  149. package/dist/plugins/sandbox-read-floor.js +608 -0
  150. package/dist/plugins/session-ownership.d.ts +2 -1
  151. package/dist/plugins/session-placement.d.ts +17 -4
  152. package/dist/plugins/session-placement.js +2 -1
  153. package/dist/plugins/session-store.d.ts +18 -20
  154. package/dist/plugins/session-store.js +4 -12
  155. package/dist/plugins/store-backend.d.ts +31 -0
  156. package/dist/plugins/store-backend.js +21 -4
  157. package/dist/plugins/store-contracts.d.ts +9 -2
  158. package/dist/plugins/tidb-pool.js +2 -1
  159. package/dist/plugins/tidb-session-store.d.ts +4 -1
  160. package/dist/plugins/tidb-session-store.js +3 -2
  161. package/dist/run-cancel-context.d.ts +34 -0
  162. package/dist/run-cancel-context.js +7 -0
  163. package/dist/runs.d.ts +39 -12
  164. package/dist/runs.js +68 -53
  165. package/dist/security.d.ts +2 -2
  166. package/dist/server-secret-env.d.ts +2 -1
  167. package/dist/server-secret-env.js +2 -4
  168. package/dist/session-error-wire.d.ts +22 -4
  169. package/dist/session-error-wire.js +16 -0
  170. package/dist/session-incarnation.d.ts +9 -3
  171. package/dist/session-sync-kernel.d.ts +42 -1
  172. package/dist/session-sync-kernel.js +17 -0
  173. package/dist/session-sync.js +16 -4
  174. package/dist/sql-engine-coordinates.d.ts +49 -0
  175. package/dist/sql-engine-coordinates.js +27 -0
  176. package/dist/standing-answer-grant.d.ts +31 -2
  177. package/dist/standing-answer-grant.js +14 -2
  178. package/dist/store-posture.d.ts +1 -1
  179. package/dist/task-a2a.d.ts +1 -1
  180. package/dist/task-mcp.d.ts +1 -1
  181. package/dist/task-settings.d.ts +52 -25
  182. package/dist/task-settings.js +91 -28
  183. package/dist/terminal.d.ts +77 -20
  184. package/dist/terminal.js +66 -20
  185. package/dist/trace/core-keyset-guard.d.ts +79 -2
  186. package/dist/trace/exit-seat.d.ts +7 -0
  187. package/dist/trace/exit-seat.js +1 -0
  188. package/dist/trace/model-segment.d.ts +2 -2
  189. package/dist/trace/redact.js +323 -90
  190. package/dist/trace/self-minted-wire.d.ts +168 -2
  191. package/dist/trace/self-minted-wire.js +63 -0
  192. package/dist/verifier-pause-card.d.ts +8 -0
  193. package/dist/verifier-pause-card.js +19 -0
  194. package/dist/web-admin-client/apply-effective.d.ts +5 -7
  195. package/dist/web-admin-client/apply-effective.js +2 -2
  196. package/dist/web-admin-client/candidate-gate.d.ts +1 -1
  197. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  198. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
  199. package/package.json +2 -2
  200. package/wire-changes/server@7.141.0.json +367 -0
@@ -19,6 +19,7 @@ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
19
19
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
20
20
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
21
21
  import { capabilitiesFor } from "../execution-lane-caps.js";
22
+ import { ADAPTER_START, BOOTSTRAP_START, buildFloorPodSeat, createSandboxReadFloor, FLOOR_K8S_CONFIGMAP_KEY, engineStart, floorArgv, } from "./sandbox-read-floor.js";
22
23
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
23
24
  import { createPosixShellFs } from "./posix-shell-fs.js";
24
25
  import { randomUUID } from "node:crypto";
@@ -202,6 +203,7 @@ function brokerContainer(image) {
202
203
  }
203
204
  export class RemoteK8sExecutionEnv {
204
205
  capabilities;
206
+ floor;
205
207
  cwd;
206
208
  podName;
207
209
  podIP;
@@ -269,7 +271,8 @@ export class RemoteK8sExecutionEnv {
269
271
  this.logger = config.logger;
270
272
  this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
271
273
  this.cacheMisses = config.cacheMisses;
272
- this.capabilities = capabilitiesFor({ remoteExec: { provider: PROVIDER, s3Snapshot: this.cfg.s3Snapshot } });
274
+ this.floor = this.deps.readFloor ?? createSandboxReadFloor({ lane: PROVIDER, knob: config.sandboxReadFloor, ...(config.logger !== undefined ? { logger: config.logger } : {}) });
275
+ this.capabilities = capabilitiesFor({ remoteExec: { provider: PROVIDER, s3Snapshot: this.cfg.s3Snapshot }, sandboxReadFloor: config.sandboxReadFloor });
273
276
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
274
277
  this.backgroundCapabilities = {
275
278
  supported: true,
@@ -667,6 +670,8 @@ export class RemoteK8sExecutionEnv {
667
670
  ];
668
671
  const pkgSourceMarked = Boolean(this.cfg.podEnv?.SEMA_PKG_SOURCE || secretEnv.SEMA_PKG_SOURCE);
669
672
  const sysctls = sandboxPodSysctls(this.cfg.ipv6);
673
+ const floorSeat = await this.ensureFloorSeat(signal);
674
+ const mounts = [...(cache ? [{ name: CACHE_VOLUME, mountPath: cache.mount }] : []), ...(floorSeat?.mounts ?? [])];
670
675
  const pod = {
671
676
  apiVersion: "v1",
672
677
  kind: "Pod",
@@ -691,7 +696,7 @@ export class RemoteK8sExecutionEnv {
691
696
  ? ["sh", "-c", "[ -f /opt/sema/pkg-source.sh ] && sh /opt/sema/pkg-source.sh || true; sleep infinity"]
692
697
  : ["sh", "-c", "sleep infinity"],
693
698
  ...(envEntries.length > 0 ? { env: envEntries } : {}),
694
- ...(cache ? { volumeMounts: [{ name: CACHE_VOLUME, mountPath: cache.mount }] } : {}),
699
+ ...(mounts.length > 0 ? { volumeMounts: mounts } : {}),
695
700
  resources: { limits: { cpu: this.cfg.cpu, memory: this.cfg.memory } },
696
701
  },
697
702
  ...(broker !== undefined ? [brokerContainer(broker.image)] : []),
@@ -699,6 +704,7 @@ export class RemoteK8sExecutionEnv {
699
704
  ...((volumes) => (volumes.length > 0 ? { volumes } : {}))([
700
705
  ...(cache ? [{ name: CACHE_VOLUME, persistentVolumeClaim: { claimName: cache.claim.name } }] : []),
701
706
  ...(leaseName !== undefined ? [{ name: BROKER_VOLUME_NAME, secret: { secretName: leaseName, items: [{ key: BROKER_LEASE_KEY, path: BROKER_LEASE_KEY }], defaultMode: 0o400 } }] : []),
707
+ ...(floorSeat?.volumes ?? []),
702
708
  ]),
703
709
  },
704
710
  };
@@ -806,7 +812,7 @@ export class RemoteK8sExecutionEnv {
806
812
  this.podIP = podIP;
807
813
  this.leaseRef = leaseName !== undefined && broker !== undefined ? { namespace: ns, podName: name, leaseSecretName: leaseName, origins: [...broker.origins] } : undefined;
808
814
  const askHome = this.homeSeat.asks;
809
- const mk = await this.runExec(`mkdir -p ${shellQuote(this.cfg.mountPath)}${askHome ? ` && { ${LANE_HOME_PROBE_COMMAND} || true; }` : ""}`);
815
+ const mk = await this.runExec(ADAPTER_START, `mkdir -p ${shellQuote(this.cfg.mountPath)}${askHome ? ` && { ${LANE_HOME_PROBE_COMMAND} || true; }` : ""}`);
810
816
  if (!mk.ok || mk.value.exitCode !== 0) {
811
817
  await this.deletePod(name).catch(() => { });
812
818
  this.podName = undefined;
@@ -825,6 +831,7 @@ export class RemoteK8sExecutionEnv {
825
831
  }
826
832
  if (cache?.fresh)
827
833
  this.logger?.warn("workspace_cache_fresh", { space: incarnationLogPrefix(cache.spaceKey), claim: cache.claim.name, size: cache.size });
834
+ await this.floor.attach(this.floorTransport(), { origin: "created", workspaceRoot: this.cfg.mountPath });
828
835
  return ok(this.workspaceHandle());
829
836
  }
830
837
  async createPodSecret(secretName, podName, podUid, values, signal) {
@@ -1184,7 +1191,7 @@ export class RemoteK8sExecutionEnv {
1184
1191
  const excludes = (this.cfg.snapshotExclude ?? []).map((item) => ` --exclude=${shellQuote(`./${item}`)}`).join("");
1185
1192
  const cmd = `tar czf ${tmp}${excludes} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
1186
1193
  `rc=$?; rm -f ${tmp}; exit $rc`;
1187
- const r = await this.runExec(cmd, { timeoutMs: boundMs });
1194
+ const r = await this.runExec(ADAPTER_START, cmd, { timeoutMs: boundMs });
1188
1195
  if (!r.ok)
1189
1196
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
1190
1197
  if (r.value.exitCode !== 0)
@@ -1220,7 +1227,7 @@ export class RemoteK8sExecutionEnv {
1220
1227
  const c = await this.provision(abortSignal);
1221
1228
  if (!c.ok)
1222
1229
  return c;
1223
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs });
1230
+ const r = await this.runExec(ADAPTER_START, cmd, { timeoutMs: this.cfg.dataTimeoutMs });
1224
1231
  const failure = !r.ok
1225
1232
  ? { cls: r.error.code === "timeout" ? "timeout" : "transport", detail: `workspace snapshot restore failed: ${this.redactUrl(r.error.message, url)}`, cause: r.error }
1226
1233
  : r.value.exitCode !== 0
@@ -1326,6 +1333,7 @@ export class RemoteK8sExecutionEnv {
1326
1333
  }
1327
1334
  async destroy() {
1328
1335
  await this.disposeBackgroundShells().catch(() => { });
1336
+ this.floor.dispose();
1329
1337
  this.destroyed = true;
1330
1338
  const lane = this.spaceLane;
1331
1339
  switch (lane.state) {
@@ -1410,7 +1418,7 @@ export class RemoteK8sExecutionEnv {
1410
1418
  async killPodProcesses() {
1411
1419
  if (!this.podName)
1412
1420
  return false;
1413
- const r = await this.runExec(buildKillAllScript(), { timeoutMs: this.cfg.rpcTimeoutMs + KILL_ALL_RECHECK_MS });
1421
+ const r = await this.runExec(ADAPTER_START, buildKillAllScript(), { timeoutMs: this.cfg.rpcTimeoutMs + KILL_ALL_RECHECK_MS });
1414
1422
  return r.ok && r.value.exitCode === 0;
1415
1423
  }
1416
1424
  async persistWorkspace(space) {
@@ -1463,23 +1471,124 @@ export class RemoteK8sExecutionEnv {
1463
1471
  await this.endSpace(space);
1464
1472
  }
1465
1473
  }
1466
- execUrl(command) {
1474
+ async ensureFloorSeat(signal) {
1475
+ const seat = buildFloorPodSeat(this.floor);
1476
+ if (seat === undefined)
1477
+ return undefined;
1478
+ if (seat.configMaps.length === 0) {
1479
+ this.floor.refusePlacement("this server carries no pinned supervisor build");
1480
+ return undefined;
1481
+ }
1482
+ for (const cm of seat.configMaps) {
1483
+ const body = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: cm.name }, immutable: true, binaryData: { [FLOOR_K8S_CONFIGMAP_KEY]: cm.binaryDataB64 } };
1484
+ let r;
1485
+ try {
1486
+ r = await this.request("POST", `/api/v1/namespaces/${this.cfg.namespace}/configmaps`, body, { signal });
1487
+ }
1488
+ catch (e) {
1489
+ this.floor.refusePlacement(`creating the supervisor ConfigMap ${cm.name} failed: ${errorMessageOf(e)}`);
1490
+ return undefined;
1491
+ }
1492
+ if (r.status !== 409 && (r.status < 200 || r.status >= 300)) {
1493
+ this.floor.refusePlacement(`creating the supervisor ConfigMap ${cm.name} was answered ${r.status}${r.status === 401 || r.status === 403 ? " (the server's service account needs create on configmaps in this namespace)" : ""}`);
1494
+ return undefined;
1495
+ }
1496
+ }
1497
+ return seat;
1498
+ }
1499
+ floorTransport() {
1500
+ return {
1501
+ bootstrap: async (command) => {
1502
+ const r = await this.runExec(BOOTSTRAP_START, command, { timeoutMs: this.cfg.rpcTimeoutMs });
1503
+ return r.ok ? { ok: true, exitCode: r.value.exitCode, stdout: r.value.stdout, stderr: r.value.stderr } : { ok: false, error: r.error.message };
1504
+ },
1505
+ place: async () => ({ ok: true }),
1506
+ openRelay: (identity, command, events) => {
1507
+ let closed = false;
1508
+ const close = (why) => {
1509
+ if (closed)
1510
+ return;
1511
+ closed = true;
1512
+ events.closed(why);
1513
+ };
1514
+ switch (identity) {
1515
+ case "root":
1516
+ queueMicrotask(() => close("a k8s exec cannot start the relay as root"));
1517
+ return { send: () => { }, close: () => { } };
1518
+ case "exec-user":
1519
+ break;
1520
+ }
1521
+ let sock;
1522
+ try {
1523
+ const { url, headers } = this.execUrl(["sh", "-c", command], true);
1524
+ sock = this.openExec(url, headers);
1525
+ }
1526
+ catch (e) {
1527
+ queueMicrotask(() => close(`the relay exec could not be opened: ${errorMessageOf(e)}`));
1528
+ return { send: () => { }, close: () => { } };
1529
+ }
1530
+ const decoder = new StringDecoder("utf8");
1531
+ let open = false;
1532
+ const pending = [];
1533
+ const write = (frame) => {
1534
+ if (sock.send === undefined)
1535
+ return close("the exec socket cannot write (no stdin channel)");
1536
+ try {
1537
+ sock.send(frame);
1538
+ }
1539
+ catch (e) {
1540
+ close(`writing to the relay failed: ${errorMessageOf(e)}`);
1541
+ }
1542
+ };
1543
+ sock.on("open", () => {
1544
+ open = true;
1545
+ for (const f of pending.splice(0))
1546
+ write(f);
1547
+ });
1548
+ sock.on("message", (...args) => {
1549
+ const data = args[0];
1550
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
1551
+ if (buf.length > 1 && buf[0] === 1)
1552
+ events.data(decoder.write(buf.subarray(1)));
1553
+ });
1554
+ sock.on("close", () => close("the relay exec closed"));
1555
+ sock.on("error", (e) => close(`the relay exec failed: ${errorMessageOf(e)}`));
1556
+ return {
1557
+ send: (line) => {
1558
+ const frame = Buffer.concat([Buffer.from([0]), Buffer.from(line, "utf8")]);
1559
+ if (open)
1560
+ write(frame);
1561
+ else
1562
+ pending.push(frame);
1563
+ },
1564
+ close: () => (sock.terminate ?? sock.close).call(sock),
1565
+ };
1566
+ },
1567
+ };
1568
+ }
1569
+ execUrl(argv, stdin = false) {
1467
1570
  const { apiUrl, token } = this.creds();
1468
1571
  const base = apiUrl.replace(/^http/, "ws");
1469
1572
  const q = new URLSearchParams();
1470
1573
  q.append("container", "sandbox");
1574
+ if (stdin)
1575
+ q.append("stdin", "true");
1471
1576
  q.append("stdout", "true");
1472
1577
  q.append("stderr", "true");
1473
- for (const part of ["sh", "-c", command])
1578
+ for (const part of argv)
1474
1579
  q.append("command", part);
1475
1580
  return { url: `${base}/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}/exec?${q.toString()}`, headers: { authorization: `Bearer ${token}` } };
1476
1581
  }
1477
- runExec(command, opts) {
1582
+ runExec(start, command, opts) {
1583
+ const launch = this.floor.wrap(start);
1584
+ if (!launch.ok)
1585
+ return Promise.resolve(launch);
1586
+ opts?.onLaunch?.(launch.value);
1478
1587
  let sock;
1479
1588
  let headers;
1480
1589
  let url;
1481
1590
  try {
1482
- ({ url, headers } = this.execUrl(command));
1591
+ ({ url, headers } = this.execUrl(floorArgv(launch.value, ["sh", "-c", command])));
1483
1592
  sock = this.openExec(url, headers);
1484
1593
  }
1485
1594
  catch (e) {
@@ -1673,12 +1782,12 @@ export class RemoteK8sExecutionEnv {
1673
1782
  });
1674
1783
  }
1675
1784
  async exec(command, options) {
1676
- return this.execCall(createCwdGuardedCall("k8s", command, this.workingDirOf(options?.cwd), options?.env), options);
1785
+ return this.execCall(engineStart(options?.readFloor), createCwdGuardedCall("k8s", command, this.workingDirOf(options?.cwd), options?.env), options);
1677
1786
  }
1678
1787
  fsExec(command, abortSignal) {
1679
- return this.execCall(createCwdGuardedCall("k8s", command, undefined, undefined), { abortSignal });
1788
+ return this.execCall(ADAPTER_START, createCwdGuardedCall("k8s", command, undefined, undefined), { abortSignal });
1680
1789
  }
1681
- async execCall(call, options) {
1790
+ async execCall(start, call, options) {
1682
1791
  const c = await this.ensureConnected();
1683
1792
  if (!c.ok)
1684
1793
  return c;
@@ -1687,7 +1796,7 @@ export class RemoteK8sExecutionEnv {
1687
1796
  const abortSignal = options?.abortSignal ? AbortSignal.any([options.abortSignal, inflight.signal]) : inflight.signal;
1688
1797
  try {
1689
1798
  if (!detachSignal) {
1690
- return call.settle(await this.runExec(call.command, {
1799
+ return call.settle(await this.runExec(start, call.command, {
1691
1800
  signal: abortSignal,
1692
1801
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
1693
1802
  boundOutput: true,
@@ -1699,7 +1808,10 @@ export class RemoteK8sExecutionEnv {
1699
1808
  const adoptId = randomUUID();
1700
1809
  const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
1701
1810
  const tee = { out: seedMemStream(""), err: seedMemStream("") };
1702
- return call.settle(await this.runExec(buildDetachCapableExec(dir, call.command), {
1811
+ let startId;
1812
+ let adoptedHere = false;
1813
+ const settled = call.settle(await this.runExec(start.kind === "engine" ? engineStart(start.readFloor, true) : start, buildDetachCapableExec(dir, call.command), {
1814
+ onLaunch: (l) => void (startId = l.mode === "run" ? l.startId : undefined),
1703
1815
  signal: abortSignal,
1704
1816
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
1705
1817
  boundOutput: true,
@@ -1716,12 +1828,17 @@ export class RemoteK8sExecutionEnv {
1716
1828
  data: (channel, payload) => feedMemStream(channel === 1 ? mem.out : mem.err, payload, K8S_BG_MEM_CAP),
1717
1829
  closed: (terminal) => ctx.onTerminal(terminal.failed, terminal.exitCode),
1718
1830
  });
1719
- return { jobId: adoptId, dir, stdoutCursor: 0, stderrCursor: 0, adopt: { mem, sock: io.sock } };
1831
+ return { jobId: adoptId, dir, stdoutCursor: 0, stderrCursor: 0, adopt: { mem, sock: io.sock }, ...(startId !== undefined ? { startId } : {}) };
1720
1832
  });
1833
+ if (adopted !== undefined)
1834
+ adoptedHere = true;
1721
1835
  return adopted?.shellId;
1722
1836
  },
1723
1837
  },
1724
1838
  }));
1839
+ if (!adoptedHere)
1840
+ this.floor.release(startId);
1841
+ return settled;
1725
1842
  }
1726
1843
  finally {
1727
1844
  this.endExec(inflight);
@@ -1759,7 +1876,7 @@ export class RemoteK8sExecutionEnv {
1759
1876
  const cap = options?.maxOutputBytes;
1760
1877
  let bytes = 0;
1761
1878
  const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
1762
- const done = this.runExec(buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command, {
1879
+ const done = this.runExec(engineStart(undefined), buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command, {
1763
1880
  signal,
1764
1881
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
1765
1882
  readTimeoutMs: options?.readTimeoutMs,
@@ -1828,7 +1945,8 @@ export class RemoteK8sExecutionEnv {
1828
1945
  }
1829
1946
  makeBgDriver() {
1830
1947
  const self = this;
1831
- const bgExec = (script, boundOutput) => self.runExec(script, { timeoutMs: self.cfg.rpcTimeoutMs, boundOutput });
1948
+ const bgExec = (start, script, boundOutput, onLaunch) => self.runExec(start, script, { timeoutMs: self.cfg.rpcTimeoutMs, boundOutput, ...(onLaunch !== undefined ? { onLaunch } : {}) });
1949
+ const under = (state) => ({ kind: "under", startId: state.startId });
1832
1950
  return {
1833
1951
  async launch(command, options, ctx) {
1834
1952
  const conn = await self.ensureConnected();
@@ -1839,14 +1957,17 @@ export class RemoteK8sExecutionEnv {
1839
1957
  const inner = buildCwdEnvCommand(command, self.workingDirOf(options?.cwd), options?.env, "raw").command;
1840
1958
  const cmdB64 = Buffer.from(inner, "utf8").toString("base64");
1841
1959
  const runnerB64 = Buffer.from(buildBgRunnerScript(dir), "utf8").toString("base64");
1842
- const r = await bgExec(buildBgLauncherScript(dir, cmdB64, runnerB64), true);
1960
+ let startId;
1961
+ const r = await bgExec(engineStart(options?.readFloor, true), buildBgLauncherScript(dir, cmdB64, runnerB64), true, (l) => void (startId = l.mode === "run" ? l.startId : undefined));
1962
+ if (!r.ok || r.value.exitCode !== 0 || !r.value.stdout.includes("SEMABG_LAUNCHED"))
1963
+ self.floor.release(startId);
1843
1964
  if (!r.ok)
1844
1965
  throw new Error(`background launch failed: ${r.error.message}`);
1845
1966
  if (r.value.exitCode !== 0 || !r.value.stdout.includes("SEMABG_LAUNCHED")) {
1846
1967
  throw new Error(`background launch exited ${r.value.exitCode}: ${r.value.stderr.slice(0, 200)}`);
1847
1968
  }
1848
1969
  void ctx;
1849
- return { jobId, dir, stdoutCursor: 0, stderrCursor: 0 };
1970
+ return { jobId, dir, stdoutCursor: 0, stderrCursor: 0, ...(startId !== undefined ? { startId } : {}) };
1850
1971
  },
1851
1972
  async read(state) {
1852
1973
  if (state.adopt) {
@@ -1859,7 +1980,7 @@ export class RemoteK8sExecutionEnv {
1859
1980
  bytesDroppedBeforeCursor: o.dropped + e.dropped,
1860
1981
  };
1861
1982
  }
1862
- const r = await bgExec(buildBgPollScript(state.dir, state.stdoutCursor, state.stderrCursor, K8S_BG_READ_CAP), false);
1983
+ const r = await bgExec(under(state), buildBgPollScript(state.dir, state.stdoutCursor, state.stderrCursor, K8S_BG_READ_CAP), false);
1863
1984
  if (!r.ok)
1864
1985
  throw new Error(`background poll failed: ${r.error.message}`);
1865
1986
  const p = parseBgPollOutput(r.value.stdout);
@@ -1874,7 +1995,7 @@ export class RemoteK8sExecutionEnv {
1874
1995
  state.stderrCursor = p.errTotal;
1875
1996
  let terminal = p.exitCode !== undefined ? { failed: false, exitCode: p.exitCode } : !p.alive ? { failed: true } : undefined;
1876
1997
  if (!terminal && (p.outTotal > K8S_BG_FILE_CAP || p.errTotal > K8S_BG_FILE_CAP)) {
1877
- void bgExec(buildBgKillScript(state.dir), true).catch(() => { });
1998
+ void bgExec(under(state), buildBgKillScript(state.dir), true).catch(() => { });
1878
1999
  terminal = { failed: true };
1879
2000
  }
1880
2001
  return {
@@ -1886,7 +2007,7 @@ export class RemoteK8sExecutionEnv {
1886
2007
  };
1887
2008
  },
1888
2009
  async kill(state) {
1889
- await bgExec(buildBgKillScript(state.dir), true).catch(() => { });
2010
+ await bgExec(under(state), buildBgKillScript(state.dir), true).catch(() => { });
1890
2011
  },
1891
2012
  async disposeOne(state) {
1892
2013
  const closeAdopted = () => {
@@ -1899,10 +2020,12 @@ export class RemoteK8sExecutionEnv {
1899
2020
  };
1900
2021
  if (self.destroyed) {
1901
2022
  closeAdopted();
2023
+ self.floor.release(state.startId);
1902
2024
  return;
1903
2025
  }
1904
- await bgExec(buildBgDisposeScript(state.dir), true).catch(() => { });
2026
+ await bgExec(under(state), buildBgDisposeScript(state.dir), true).catch(() => { });
1905
2027
  closeAdopted();
2028
+ self.floor.release(state.startId);
1906
2029
  },
1907
2030
  };
1908
2031
  }
@@ -2013,7 +2136,7 @@ export class RemoteK8sExecutionEnv {
2013
2136
  const c = await this.ensureConnected();
2014
2137
  if (!c.ok)
2015
2138
  return c;
2016
- return this.runExec(command, { signal: abortSignal, capMs: this.cfg.dataTimeoutMs });
2139
+ return this.runExec(ADAPTER_START, command, { signal: abortSignal, capMs: this.cfg.dataTimeoutMs });
2017
2140
  }
2018
2141
  workingDirOf(cwd) {
2019
2142
  const dir = cwd ?? this.cwd;
@@ -1,6 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { type LaneHomeConfig } from "../lane-home-probe.js";
3
- import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory } from "@sema-agent/core";
3
+ import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type ExecutionEnvCapabilities } from "@sema-agent/core";
4
+ import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
5
+ import { type SandboxReadFloor } from "./sandbox-read-floor.js";
4
6
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
5
7
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
6
8
  /** Construction config for {@link RemoteLocalDockerExecutionEnv}. */
@@ -55,14 +57,20 @@ export interface LocalDockerEnvConfig extends LaneHomeConfig {
55
57
  /** Stable id mixed into the container NAME (e.g. the sessionId). A random suffix is always appended so two
56
58
  * envs sharing an id never collide. Default = a random id. */
57
59
  id?: string;
60
+ /** `SANDBOX_READ_FLOOR`(keep | off;缺席 ⇒ keep):本容器守不守读拒表的字节地板 = 读地板模块服务本车道 ∧ 本键不是 off(`sandbox-read-floor.ts`)。 */
61
+ sandboxReadFloor?: SandboxReadFloorKnob;
58
62
  }
59
63
  /** Inject `spawn` for tests (a fake docker CLI). Defaults to node's child_process spawn. */
60
64
  export interface LocalDockerEnvDeps {
61
65
  spawn?: typeof spawn;
66
+ /** 这只环境手里的读地板模块(缺席 ⇒ `createSandboxReadFloor` 按旋钮选;钉注入守地板的模块)。能力块的 `keepsReadFloor` 读它的状态。 */
67
+ readFloor?: SandboxReadFloor;
62
68
  }
63
69
  export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv {
64
- /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
65
- readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
70
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。读地板旋钮是它的输入,{@link floor} 由同一只判据、同一个旋钮值选出。 */
71
+ readonly capabilities: ExecutionEnvCapabilities;
72
+ /** 读拒表字节地板模块:容器里每一次 `docker exec` 起动都先经它的 `wrap`({@link startArgv},唯一接缝)。 */
73
+ private readonly floor;
66
74
  /** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
67
75
  cwd: string;
68
76
  /** core `ExecutionEnv.homeDir`([ref]):本容器里执行用户的 home —— 声明在场 ⇒ 声明(开环境那一拍与 `$HOME` 对证过),否则开容器那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
@@ -159,7 +167,7 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
159
167
  }): Promise<Result<string[], FileError>>;
160
168
  /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;父目录与私有暂存目录由编排的准备那一步建):
161
169
  * 上传 = 容器内 `cat` 从 stdin 收整份字节落到私有目录里的暂存件({@link pipeIn});提交 = 容器内一次 shell 往返。两者同一身份 ——
162
- * 同一只 argv 构造({@link execArgv}),都是镜像的用户。不用 `docker cp`:它由守护进程以 root 落盘,镜像用户 ≠ root 时暂存件属 root、
170
+ * 同一只 argv 构造({@link startArgv}),都是镜像的用户。不用 `docker cp`:它由守护进程以 root 落盘,镜像用户 ≠ root 时暂存件属 root、
163
171
  * 提交方拿不走也清不掉,「目录不收新名字」也探不出来(root 哪里都写得进)。 */
164
172
  private stagedLane;
165
173
  /** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `docker cp` 直落目标(以守护进程的 root 身份):传输中途断 / 取消时目标剩什么取决于守护进程的解包实现,本适配器不依赖它。 */
@@ -205,17 +213,23 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
205
213
  /**
206
214
  * 交给宿主 `docker` CLI 的 env —— 宿主子进程 env 的唯一组合(`server-secret-env.ts` `hostSubprocessEnv`),继承词 `"all"`:本服务自有凭据(起服声明的名与值)
207
215
  * 任何词都不交,CLI 与它拉起的凭据助手 / CLI 插件要读的变量(`DOCKER_*` / `AWS_*` …)照交 —— 形状剥会剥 `AWS_SECRET_ACCESS_KEY` 一类,ECR / GCR 凭据助手因此断。
208
- * 命令本身跑在容器里,容器的 env 只由 `execArgv` 的 `-e K=V` 显式给,本 env 到不了容器。每次起进程现算一次(与 host shell 同)。
216
+ * 命令本身跑在容器里,容器的 env 只由 `startArgv` 的 `-e K=V` 显式给,本 env 到不了容器。每次起进程现算一次(与 host shell 同)。
209
217
  */
210
218
  private cliEnv;
211
219
  /** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
212
220
  private withHostFlag;
213
- /** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
214
- * argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's `/bin/sh -c`.
215
- * `stdin` ⇒ `-i` (the payload of {@link pipeIn}); nothing else differs, so a piped upload runs as the same user as every other exec. */
216
- private execArgv;
217
- /** Like {@link execArgv} but for an internal FileSystem helper command (uses the workspace cwd + adapter env). */
218
- private execRaw;
221
+ /**
222
+ * **唯一起动接缝**:容器里的每一次 `docker exec` 起动(引擎的命令、文件面助手、探针、地板的引导命令)都从这里拿 argv —— 先问地板模块
223
+ * `wrap(起动形)`,再按答案编码(身份旗标 + 监督者命令行头;不守地板 ⇒ 与改前逐字同)。本适配器对地板零判断;模块拒 ⇒ `spawn_error`,命令从未起。
224
+ * argv elements are SEPARATE (no host-side shell join) — the command runs in the CONTAINER's shell (`-c`). `stdin` ⇒ `-i` (the payload of
225
+ * {@link pipeIn}); nothing else differs, so a piped upload runs as the same user as every other exec. cwd (`-w`) and per-command env (`-e K=V`;
226
+ * adapter base env under, per-command wins) — except `containerDefaults`: the command asks about the container's OWN defaults (the shell probe),
227
+ * so it carries neither (the bare argv is then `exec <name> <shell> -c <cmd>`, byte-identical to the pre-floor probe).
228
+ */
229
+ private startArgv;
230
+ /** 地板模块的三件原语(引导命令 / 放置 / 中继)。引导与放置经 {@link startArgv}(起动形 `bootstrap`:只在附着中合法);中继以模块给的身份起一只
231
+ * 长连的 `docker exec -i`(stdin / stdout 作双工),随本环境 `destroy` 由模块关。 */
232
+ private floorTransport;
219
233
  /** Spawn the docker binary with argv; collect stdout/stderr (+ raw bytes when binary), bound by timeout/abort. */
220
234
  private docker;
221
235
  /** Lazy connect for an exec path → ExecutionError on failure (fail closed, never throws). */
@@ -9,6 +9,7 @@ import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
9
9
  import { buildCapturedOutput, createCutError } from "../exec-cut.js";
10
10
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
11
11
  import { capabilitiesFor } from "../execution-lane-caps.js";
12
+ import { ADAPTER_START, BOOTSTRAP_START, createSandboxReadFloor, dockerIdentityFlags, engineStart, floorArgv, } from "./sandbox-read-floor.js";
12
13
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
13
14
  import { createPosixShellFs } from "./posix-shell-fs.js";
14
15
  const PROVIDER = "local-docker";
@@ -21,7 +22,8 @@ const unsupported = (op) => ({
21
22
  error: new RemoteExecutionError("unsupported", `${op} is not supported on a local-docker target (v1: a container is an isolation boundary but not a managed memory snapshot; capabilities.suspendable=false)`),
22
23
  });
23
24
  export class RemoteLocalDockerExecutionEnv {
24
- capabilities = capabilitiesFor({ remoteExec: { provider: "local-docker" } });
25
+ capabilities;
26
+ floor;
25
27
  cwd;
26
28
  get homeDir() {
27
29
  return this.homeSeat.home;
@@ -52,6 +54,8 @@ export class RemoteLocalDockerExecutionEnv {
52
54
  this.cwd = this.cfg.mountPath;
53
55
  this.homeSeat = new LaneHomeSeat(PROVIDER, config);
54
56
  this.spawnFn = deps.spawn ?? spawn;
57
+ this.floor = deps.readFloor ?? createSandboxReadFloor({ lane: PROVIDER, knob: config.sandboxReadFloor });
58
+ this.capabilities = capabilitiesFor({ remoteExec: { provider: PROVIDER }, sandboxReadFloor: config.sandboxReadFloor });
55
59
  const id = config.id ?? randomBytes(6).toString("hex");
56
60
  const suffix = randomBytes(4).toString("hex");
57
61
  this.containerName = `sema-docker-${sanitizeId(id)}-${suffix}`;
@@ -130,6 +134,7 @@ export class RemoteLocalDockerExecutionEnv {
130
134
  await this.forceRemove().catch(() => { });
131
135
  return { ok: false, error: home };
132
136
  }
137
+ await this.floor.attach(this.floorTransport(), { origin: "created", workspaceRoot: this.cfg.mountPath });
133
138
  if (this.destroyed) {
134
139
  await this.forceRemove().catch(() => { });
135
140
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
@@ -143,7 +148,8 @@ export class RemoteLocalDockerExecutionEnv {
143
148
  async homeVerdict(signal) {
144
149
  if (!this.homeSeat.asks)
145
150
  return undefined;
146
- const probe = await this.docker(this.execRaw(LANE_HOME_PROBE_COMMAND), { signal, timeoutMs: this.cfg.controlTimeoutMs });
151
+ const argv = this.startArgv(ADAPTER_START, LANE_HOME_PROBE_COMMAND);
152
+ const probe = argv.ok ? await this.docker(argv.value, { signal, timeoutMs: this.cfg.controlTimeoutMs }) : argv;
147
153
  if (!probe.ok && probe.error.code === "aborted")
148
154
  return new RemoteExecutionError("aborted", "connect aborted while reading the environment's home");
149
155
  const verdict = this.homeSeat.settle(probe.ok && probe.value.exitCode === 0 ? { ok: true, stdout: probe.value.stdout } : { ok: false, reason: probe.ok ? `exit ${probe.value.exitCode}` : probe.error.message });
@@ -199,6 +205,7 @@ export class RemoteLocalDockerExecutionEnv {
199
205
  this.connecting = undefined;
200
206
  this.containerId = undefined;
201
207
  await inflight?.catch(() => { });
208
+ this.floor.dispose();
202
209
  await this.forceRemove().catch(() => { });
203
210
  }
204
211
  async cleanup() {
@@ -213,8 +220,10 @@ export class RemoteLocalDockerExecutionEnv {
213
220
  const ready = await this.ensureConnectedExec();
214
221
  if (!ready.ok)
215
222
  return ready;
216
- const execArgs = this.execArgv(command, options?.cwd, options?.env);
217
- const r = await this.docker(execArgs, {
223
+ const execArgs = this.startArgv(engineStart(options?.readFloor), command, { cwd: options?.cwd, env: options?.env });
224
+ if (!execArgs.ok)
225
+ return execArgs;
226
+ const r = await this.docker(execArgs.value, {
218
227
  signal: options?.abortSignal,
219
228
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs,
220
229
  onStdout: options?.onStdout,
@@ -228,8 +237,10 @@ export class RemoteLocalDockerExecutionEnv {
228
237
  const ready = await this.ensureConnectedExec();
229
238
  if (!ready.ok)
230
239
  throw new RemoteExecutionError("connect_failed", ready.error.message, ready.error);
231
- const execArgs = this.execArgv(command, options?.cwd, options?.env);
232
- const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(execArgs), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
240
+ const execArgs = this.startArgv(engineStart(undefined), command, { cwd: options?.cwd, env: options?.env });
241
+ if (!execArgs.ok)
242
+ throw new RemoteExecutionError("unsupported", execArgs.error.message, execArgs.error);
243
+ const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(execArgs.value), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
233
244
  const queue = [];
234
245
  let finished = false;
235
246
  let failure;
@@ -361,7 +372,8 @@ export class RemoteLocalDockerExecutionEnv {
361
372
  if (!ready.ok)
362
373
  return ready;
363
374
  const abs = this.resolve(p);
364
- const r = await this.docker(this.execRaw(`cat ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, binary: true });
375
+ const argv = this.startArgv(ADAPTER_START, `cat ${shellQuote(abs)}`);
376
+ const r = argv.ok ? await this.docker(argv.value, { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, binary: true }) : argv;
365
377
  if (!r.ok)
366
378
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
367
379
  if (r.value.exitCode !== 0)
@@ -423,7 +435,10 @@ export class RemoteLocalDockerExecutionEnv {
423
435
  if (!c.ok)
424
436
  return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`, c.error) };
425
437
  }
426
- return this.docker(this.execArgv(cmd, undefined, undefined, input !== undefined), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
438
+ const argv = this.startArgv(ADAPTER_START, cmd, { stdin: input !== undefined });
439
+ if (!argv.ok)
440
+ return argv;
441
+ return this.docker(argv.value, { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
427
442
  }
428
443
  async fileInfo(p, abortSignal) {
429
444
  return this.posixFs.statFileInfo(p, abortSignal);
@@ -461,18 +476,68 @@ export class RemoteLocalDockerExecutionEnv {
461
476
  withHostFlag(args) {
462
477
  return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
463
478
  }
464
- execArgv(command, cwd, env, stdin = false) {
465
- const args = stdin ? ["exec", "-i"] : ["exec"];
466
- const dir = cwd != null ? this.resolve(cwd) : this.cwd;
467
- args.push("-w", dir);
468
- const mergedEnv = { ...(this.cfg.env ?? {}), ...(env ?? {}) };
469
- for (const [k, v] of Object.entries(mergedEnv))
470
- args.push("-e", `${k}=${v}`);
471
- args.push(this.containerName, this.containerShell, "-c", command);
472
- return args;
473
- }
474
- execRaw(command) {
475
- return this.execArgv(command);
479
+ startArgv(start, command, o = {}) {
480
+ const launch = this.floor.wrap(start);
481
+ if (!launch.ok)
482
+ return launch;
483
+ const args = o.stdin ? ["exec", "-i"] : ["exec"];
484
+ args.push(...dockerIdentityFlags(launch.value));
485
+ if (o.containerDefaults !== true) {
486
+ args.push("-w", o.cwd != null ? this.resolve(o.cwd) : this.cwd);
487
+ const mergedEnv = { ...(this.cfg.env ?? {}), ...(o.env ?? {}) };
488
+ for (const [k, v] of Object.entries(mergedEnv))
489
+ args.push("-e", `${k}=${v}`);
490
+ }
491
+ args.push(this.containerName, ...floorArgv(launch.value, [o.shell ?? this.containerShell, "-c", command]));
492
+ return ok(args);
493
+ }
494
+ floorTransport() {
495
+ const control = { timeoutMs: this.cfg.controlTimeoutMs };
496
+ return {
497
+ bootstrap: async (command) => {
498
+ const argv = this.startArgv(BOOTSTRAP_START, command, { shell: "/bin/sh" });
499
+ if (!argv.ok)
500
+ return { ok: false, error: argv.error.message };
501
+ const r = await this.docker(argv.value, control);
502
+ return r.ok ? { ok: true, exitCode: r.value.exitCode, stdout: r.value.stdout, stderr: r.value.stderr } : { ok: false, error: r.error.message };
503
+ },
504
+ place: async (dest, bytes) => {
505
+ const dir = path.posix.dirname(dest);
506
+ const tmp = `${dest}.${randomBytes(6).toString("hex")}`;
507
+ const cmd = `umask 022 && mkdir -p ${shellQuote(dir)} && cat > ${shellQuote(tmp)} && chmod 0755 ${shellQuote(tmp)} && mv -f ${shellQuote(tmp)} ${shellQuote(dest)}`;
508
+ const argv = this.startArgv(BOOTSTRAP_START, cmd, { shell: "/bin/sh", stdin: true });
509
+ if (!argv.ok)
510
+ return { ok: false, error: argv.error.message };
511
+ const r = await this.docker(argv.value, { ...control, input: bytes });
512
+ if (!r.ok)
513
+ return { ok: false, error: r.error.message };
514
+ return r.value.exitCode === 0 ? { ok: true } : { ok: false, error: r.value.stderr.trim() || `exit ${r.value.exitCode}` };
515
+ },
516
+ openRelay: (identity, command, events) => {
517
+ const args = ["exec", "-i", ...dockerIdentityFlags({ mode: "control", identity }), this.containerName, "/bin/sh", "-c", command];
518
+ const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["pipe", "pipe", "pipe"], env: this.cliEnv() });
519
+ let closed = false;
520
+ const close = (why) => {
521
+ if (closed)
522
+ return;
523
+ closed = true;
524
+ events.closed(why);
525
+ };
526
+ child.stdout?.setEncoding("utf8");
527
+ child.stdout?.on("data", (d) => events.data(d));
528
+ child.stderr?.on("data", () => { });
529
+ child.stdin?.on("error", () => { });
530
+ child.on("error", (e) => close(`docker exec failed: ${e.message}`));
531
+ child.on("close", (code, signal) => close(`relay exited (${code ?? signal ?? "unknown"})`));
532
+ return {
533
+ send: (line) => void child.stdin?.write(line),
534
+ close: () => {
535
+ child.stdin?.end();
536
+ child.kill("SIGKILL");
537
+ },
538
+ };
539
+ },
540
+ };
476
541
  }
477
542
  docker(args, opts) {
478
543
  return new Promise((resolve) => {
@@ -571,7 +636,8 @@ export class RemoteLocalDockerExecutionEnv {
571
636
  }
572
637
  async detectContainerShell() {
573
638
  try {
574
- const r = await this.docker(["exec", this.containerName, "/bin/sh", "-c", "command -v bash"], { timeoutMs: this.cfg.controlTimeoutMs });
639
+ const argv = this.startArgv(ADAPTER_START, "command -v bash", { shell: "/bin/sh", containerDefaults: true });
640
+ const r = argv.ok ? await this.docker(argv.value, { timeoutMs: this.cfg.controlTimeoutMs }) : argv;
575
641
  if (r.ok && r.value.exitCode === 0 && r.value.stdout.trim().length > 0)
576
642
  this.containerShell = "bash";
577
643
  }