@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.
- package/MIGRATION.md +290 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +75 -13
- package/dist/approval-batch-census.d.ts +27 -0
- package/dist/approval-batch-census.js +27 -0
- package/dist/approval-card.d.ts +31 -16
- package/dist/approval-card.js +6 -3
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +10 -0
- package/dist/audit.d.ts +4 -2
- package/dist/audit.js +2 -2
- package/dist/boot/device-lane.d.ts +11 -0
- package/dist/boot/device-lane.js +16 -0
- package/dist/boot/engine-lease.d.ts +24 -3
- package/dist/boot/engine-lease.js +9 -2
- package/dist/boot/execution-env.js +8 -4
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/memory-consolidation.js +1 -1
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/resolve-spec.js +7 -4
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +4 -3
- package/dist/boot/shutdown.js +5 -0
- package/dist/boot/stage-05-execution-env.js +3 -1
- package/dist/boot/stage-06-runners.d.ts +0 -2
- package/dist/boot/stage-06-runners.js +1 -1
- package/dist/boot/stage-07-capability-layer.d.ts +0 -1
- package/dist/boot/stage-07-capability-layer.js +8 -10
- package/dist/boot/stage-08-reapers.d.ts +0 -1
- package/dist/boot/stage-09-leader.d.ts +0 -1
- package/dist/boot/stage-10-http-server.d.ts +0 -1
- package/dist/boot/stage-10-http-server.js +1 -0
- package/dist/boot/stores.js +10 -4
- package/dist/boot/web-admin.js +1 -0
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +9 -0
- package/dist/capability-facts.js +9 -0
- package/dist/card-void.d.ts +38 -0
- package/dist/card-void.js +23 -0
- package/dist/config-catalog.js +11 -8
- package/dist/config-provider.d.ts +13 -13
- package/dist/config-provider.js +19 -33
- package/dist/config-types.d.ts +26 -8
- package/dist/config.js +55 -46
- package/dist/delegated-session-owner.d.ts +21 -19
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +3 -0
- package/dist/execution-config-root.d.ts +22 -0
- package/dist/execution-config-root.js +35 -0
- package/dist/execution-lane-caps.d.ts +49 -2
- package/dist/execution-lane-caps.js +90 -13
- package/dist/execution-placement.d.ts +3 -0
- package/dist/execution-placement.js +7 -0
- package/dist/fleet/drain-entry.d.ts +3 -0
- package/dist/hosted-posture.d.ts +8 -0
- package/dist/hosted-posture.js +4 -0
- package/dist/http/admission.js +3 -3
- package/dist/http/caller-trust.d.ts +3 -2
- package/dist/http/caller-trust.js +2 -1
- package/dist/http/dispatch.js +4 -0
- package/dist/http/live-frame.d.ts +24 -0
- package/dist/http/live-frame.js +16 -0
- package/dist/http/resume-legs.js +37 -20
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +5 -4
- package/dist/http/routes/diagnostics.d.ts +0 -19
- package/dist/http/routes/diagnostics.js +16 -2
- package/dist/http/routes/memory-bundle.d.ts +2 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/run-permission-mode.d.ts +75 -0
- package/dist/http/routes/run-permission-mode.js +170 -0
- package/dist/http/routes/session-sync.d.ts +3 -6
- package/dist/http/routes/session-sync.js +7 -2
- package/dist/http/routes/sessions.js +5 -4
- package/dist/http/routes/tasks.js +63 -66
- package/dist/http/run-read-face.js +3 -3
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +2 -2
- package/dist/http/server.d.ts +9 -2
- package/dist/http/sse-log.d.ts +3 -8
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +3 -1
- package/dist/instruction-source-notice.d.ts +7 -0
- package/dist/instruction-source-notice.js +6 -6
- package/dist/mcp-assertion-header.d.ts +4 -0
- package/dist/mcp-assertion-header.js +5 -0
- package/dist/memory-posture.d.ts +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +1 -1
- package/dist/memory-sync-client.d.ts +14 -2
- package/dist/memory-sync-client.js +80 -18
- package/dist/observability/fail-open.d.ts +30 -2
- package/dist/observability/fail-open.js +30 -2
- package/dist/observability/metrics.js +0 -1
- package/dist/observability/run-terminal.d.ts +6 -2
- package/dist/observability/run-terminal.js +25 -26
- package/dist/observability/secret-env-scrub.d.ts +20 -1
- package/dist/observability/secret-env-scrub.js +14 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
- package/dist/orchestration/workflow-completion-inbox.js +13 -5
- package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -1
- package/dist/permission-mode-admission.d.ts +68 -0
- package/dist/permission-mode-admission.js +23 -0
- package/dist/permission-rule-list.d.ts +55 -6
- package/dist/permission-rule-list.js +52 -5
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
- package/dist/plugins/checkpoint-store-sql.js +74 -3
- package/dist/plugins/device-store-sql.d.ts +5 -2
- package/dist/plugins/device-store-sql.js +4 -1
- package/dist/plugins/execution-placement-sql.d.ts +11 -1
- package/dist/plugins/execution-placement-sql.js +12 -1
- package/dist/plugins/file-run-store.d.ts +2 -2
- package/dist/plugins/file-run-store.js +1 -1
- package/dist/plugins/fork-routing-session-store.d.ts +10 -32
- package/dist/plugins/fork-routing-session-store.js +2 -61
- package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
- package/dist/plugins/k8s-exec-protocol.js +6 -1
- package/dist/plugins/local-checkpoint-store.d.ts +10 -1
- package/dist/plugins/local-checkpoint-store.js +22 -0
- package/dist/plugins/local-session-store.d.ts +227 -35
- package/dist/plugins/local-session-store.js +518 -37
- package/dist/plugins/local-session-window.d.ts +17 -3
- package/dist/plugins/local-session-window.js +7 -13
- package/dist/plugins/memory-run-store.d.ts +2 -2
- package/dist/plugins/memory-run-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +42 -2
- package/dist/plugins/one-time-migrations.js +65 -3
- package/dist/plugins/pg-pool.js +3 -2
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +3 -2
- package/dist/plugins/remote-env-e2b.d.ts +24 -3
- package/dist/plugins/remote-env-e2b.js +131 -18
- package/dist/plugins/remote-env-k8s.d.ts +20 -1
- package/dist/plugins/remote-env-k8s.js +148 -25
- package/dist/plugins/remote-env-local-docker.d.ts +25 -11
- package/dist/plugins/remote-env-local-docker.js +87 -21
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +4 -3
- package/dist/plugins/sandbox-read-floor.d.ts +273 -0
- package/dist/plugins/sandbox-read-floor.js +608 -0
- package/dist/plugins/session-ownership.d.ts +2 -1
- package/dist/plugins/session-placement.d.ts +17 -4
- package/dist/plugins/session-placement.js +2 -1
- package/dist/plugins/session-store.d.ts +18 -20
- package/dist/plugins/session-store.js +4 -12
- package/dist/plugins/store-backend.d.ts +31 -0
- package/dist/plugins/store-backend.js +21 -4
- package/dist/plugins/store-contracts.d.ts +9 -2
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-session-store.d.ts +4 -1
- package/dist/plugins/tidb-session-store.js +3 -2
- package/dist/run-cancel-context.d.ts +34 -0
- package/dist/run-cancel-context.js +7 -0
- package/dist/runs.d.ts +39 -12
- package/dist/runs.js +68 -53
- package/dist/security.d.ts +2 -2
- package/dist/server-secret-env.d.ts +2 -1
- package/dist/server-secret-env.js +2 -4
- package/dist/session-error-wire.d.ts +22 -4
- package/dist/session-error-wire.js +16 -0
- package/dist/session-incarnation.d.ts +9 -3
- package/dist/session-sync-kernel.d.ts +42 -1
- package/dist/session-sync-kernel.js +17 -0
- package/dist/session-sync.js +16 -4
- package/dist/sql-engine-coordinates.d.ts +49 -0
- package/dist/sql-engine-coordinates.js +27 -0
- package/dist/standing-answer-grant.d.ts +31 -2
- package/dist/standing-answer-grant.js +14 -2
- package/dist/store-posture.d.ts +1 -1
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +52 -25
- package/dist/task-settings.js +91 -28
- package/dist/terminal.d.ts +77 -20
- package/dist/terminal.js +66 -20
- package/dist/trace/core-keyset-guard.d.ts +79 -2
- package/dist/trace/exit-seat.d.ts +7 -0
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/model-segment.d.ts +2 -2
- package/dist/trace/redact.js +323 -90
- package/dist/trace/self-minted-wire.d.ts +168 -2
- package/dist/trace/self-minted-wire.js +63 -0
- package/dist/verifier-pause-card.d.ts +8 -0
- package/dist/verifier-pause-card.js +19 -0
- package/dist/web-admin-client/apply-effective.d.ts +5 -7
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +1 -1
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
- package/package.json +2 -2
- 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.
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 只由 `
|
|
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
|
-
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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
|
|
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.
|
|
217
|
-
|
|
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.
|
|
232
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
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
|
}
|