@sema-agent/server 7.141.0 → 7.142.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 +215 -0
- package/USAGE.md +67 -12
- package/dist/approval-ask-audit-store.d.ts +54 -38
- package/dist/approval-ask-audit-store.js +58 -39
- package/dist/approval-card.d.ts +8 -8
- package/dist/approval-crash-convergence.d.ts +17 -7
- package/dist/approval-crash-convergence.js +94 -22
- package/dist/approval-deny-reasons.d.ts +34 -30
- package/dist/approval-deny-reasons.js +21 -6
- package/dist/approval-policy-settlement.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +33 -1
- package/dist/approval-reconciler.js +43 -40
- package/dist/auth-bridge.d.ts +9 -6
- package/dist/auth-bridge.js +9 -3
- package/dist/boot/adoption.js +2 -1
- package/dist/boot/budget-tracing.d.ts +16 -0
- package/dist/boot/budget-tracing.js +10 -6
- package/dist/boot/device-lane.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +24 -4
- package/dist/boot/execution-env.js +26 -23
- package/dist/boot/execution-lane.js +2 -1
- package/dist/boot/exit-boundary.js +5 -4
- package/dist/boot/memory-consolidation.js +2 -1
- package/dist/boot/parked-revive-gate.js +2 -1
- package/dist/boot/permission-rules-audit.d.ts +0 -28
- package/dist/boot/permission-rules-audit.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -1
- package/dist/boot/reapers.js +4 -3
- package/dist/boot/resolve-spec.js +11 -2
- package/dist/boot/retention-lane.js +5 -3
- package/dist/boot/runner-deps.js +16 -13
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.js +4 -3
- package/dist/boot/shutdown.js +3 -2
- package/dist/boot/stage-04-stores.js +3 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.js +8 -6
- package/dist/boot/stage-06-runners.d.ts +1 -1
- package/dist/boot/stage-06-runners.js +2 -1
- package/dist/boot/stage-07-capability-layer.d.ts +1 -1
- package/dist/boot/stage-07-capability-layer.js +2 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -1
- package/dist/boot/stage-08-reapers.js +3 -2
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +5 -4
- package/dist/boot/stores.js +3 -2
- package/dist/boot/web-admin.js +17 -17
- package/dist/boot/webfetch-summarize-lane.js +2 -1
- package/dist/boot/workflow-orchestration.js +3 -2
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/builtin-tools.js +2 -4
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capability-facts.d.ts +24 -0
- package/dist/capability-facts.js +23 -0
- package/dist/config-catalog.js +1 -0
- package/dist/config-types.d.ts +15 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +17 -3
- package/dist/execution-lane-caps.d.ts +2 -1
- package/dist/fleet/fleet-bus.d.ts +6 -3
- package/dist/hooks/hook-runner.d.ts +6 -0
- package/dist/hooks/hook-runner.js +30 -1
- package/dist/http/dispatch.js +4 -2
- package/dist/http/execution-context.d.ts +2 -2
- package/dist/http/idempotency.d.ts +1 -1
- package/dist/http/principal-gate.d.ts +3 -1
- package/dist/http/principal-gate.js +17 -8
- package/dist/http/resume-legs.js +5 -9
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/attachments.js +18 -0
- package/dist/http/routes/capabilities.js +5 -2
- package/dist/http/routes/memory-policy.js +26 -6
- package/dist/http/routes/runs.js +3 -3
- package/dist/http/routes/session-memory-sync.d.ts +63 -0
- package/dist/http/routes/session-memory-sync.js +157 -0
- package/dist/http/routes/sessions-list.d.ts +2 -1
- package/dist/http/routes/tasks.js +7 -5
- package/dist/http/run-read-face.d.ts +7 -0
- package/dist/http/run-read-face.js +9 -3
- package/dist/http/send.d.ts +5 -0
- package/dist/http/send.js +9 -2
- package/dist/http/server.d.ts +14 -8
- package/dist/http/server.js +2 -4
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +2 -2
- package/dist/http/sync-submit-body.d.ts +21 -4
- package/dist/http/sync-submit-body.js +13 -0
- package/dist/identity-source.d.ts +21 -2
- package/dist/identity-source.js +6 -0
- package/dist/memory-incident-sink.d.ts +19 -6
- package/dist/memory-incident-sink.js +8 -4
- package/dist/memory-sync-client.d.ts +21 -21
- package/dist/memory-sync-client.js +50 -42
- package/dist/memory-sync-identity.d.ts +24 -0
- package/dist/memory-sync-identity.js +57 -0
- package/dist/memory-sync.d.ts +7 -1
- package/dist/memory-sync.js +6 -3
- package/dist/observability/fail-open.d.ts +27 -10
- package/dist/observability/fail-open.js +20 -7
- package/dist/observability/run-terminal.d.ts +92 -34
- package/dist/observability/run-terminal.js +167 -162
- package/dist/plugins/approval-ask-store-file.d.ts +14 -4
- package/dist/plugins/approval-ask-store-file.js +48 -7
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +9 -8
- package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
- package/dist/plugins/approval-ask-store-sql.js +49 -19
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +11 -1
- package/dist/plugins/local-session-store.d.ts +36 -7
- package/dist/plugins/local-session-store.js +76 -32
- package/dist/plugins/local-task-attachment-store.d.ts +12 -2
- package/dist/plugins/local-task-attachment-store.js +67 -43
- package/dist/plugins/memory-key-guards.d.ts +3 -3
- package/dist/plugins/memory-run-store.d.ts +6 -1
- package/dist/plugins/memory-run-store.js +11 -1
- package/dist/plugins/one-time-migrations.d.ts +27 -1
- package/dist/plugins/one-time-migrations.js +57 -3
- package/dist/plugins/remote-env-e2b.d.ts +23 -4
- package/dist/plugins/remote-env-e2b.js +111 -28
- package/dist/plugins/remote-env-k8s.d.ts +31 -5
- package/dist/plugins/remote-env-k8s.js +86 -19
- package/dist/plugins/remote-env-local-docker.d.ts +15 -3
- package/dist/plugins/remote-env-local-docker.js +40 -12
- package/dist/plugins/run-store-sql.d.ts +9 -1
- package/dist/plugins/run-store-sql.js +9 -1
- package/dist/plugins/store-backend.d.ts +10 -7
- package/dist/plugins/store-backend.js +1 -0
- package/dist/plugins/store-contracts.d.ts +26 -1
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +112 -14
- package/dist/plugins/task-attachment-store.js +221 -28
- package/dist/plugins/workspace-fill.d.ts +60 -0
- package/dist/plugins/workspace-fill.js +106 -0
- package/dist/project-bundle.d.ts +38 -0
- package/dist/project-bundle.js +609 -0
- package/dist/run-local.js +2 -8
- package/dist/runs.js +1 -2
- package/dist/sandbox-egress-policy.d.ts +4 -1
- package/dist/sandbox-egress-policy.js +3 -1
- package/dist/sandbox-git-plane.d.ts +31 -12
- package/dist/sandbox-git-plane.js +24 -4
- package/dist/security.d.ts +9 -0
- package/dist/security.js +8 -0
- package/dist/session-error-wire.d.ts +24 -0
- package/dist/session-error-wire.js +7 -0
- package/dist/task-settings.d.ts +10 -2
- package/dist/task-settings.js +15 -2
- package/dist/terminal.d.ts +39 -19
- package/dist/terminal.js +65 -22
- package/dist/tool-approval.d.ts +4 -3
- package/dist/tool-approval.js +12 -14
- package/dist/trace/core-keyset-guard.d.ts +123 -6
- package/dist/trace/engine-notice-wire.d.ts +8 -2
- package/dist/trace/engine-notice-wire.js +4 -2
- package/dist/trace/exit-seat.d.ts +2 -1
- package/dist/trace/ledger-sink.d.ts +8 -4
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/project.d.ts +18 -1
- package/dist/trace/project.js +16 -0
- package/dist/trace/self-minted-wire.d.ts +251 -2
- package/dist/trace/self-minted-wire.js +82 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/web-admin-client/apply-effective.js +21 -1
- package/dist/web-admin-client/effective-keys.js +2 -0
- package/dist/web-admin-client/types.d.ts +4 -0
- package/package.json +1 -1
- package/wire-changes/server@7.142.0.json +639 -0
|
@@ -35,7 +35,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
35
35
|
handle;
|
|
36
36
|
destroyed = false;
|
|
37
37
|
homeSeat;
|
|
38
|
-
|
|
38
|
+
connectRefusal;
|
|
39
39
|
connecting;
|
|
40
40
|
activeCommands = 0;
|
|
41
41
|
paused = false;
|
|
@@ -138,10 +138,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
workspaceHandle() {
|
|
141
|
-
|
|
142
|
-
throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
|
|
143
|
-
}
|
|
144
|
-
return this.handle;
|
|
141
|
+
return this.handle ?? { sandboxId: "(unconnected)", provider: PROVIDER, mountPath: this.cwd };
|
|
145
142
|
}
|
|
146
143
|
async getHost(port) {
|
|
147
144
|
const conn = await this.ensureConnected();
|
|
@@ -162,17 +159,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
162
159
|
}
|
|
163
160
|
if (this.destroyed)
|
|
164
161
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
165
|
-
if (this.
|
|
166
|
-
return { ok: false, error: this.
|
|
162
|
+
if (this.connectRefusal)
|
|
163
|
+
return { ok: false, error: this.connectRefusal };
|
|
167
164
|
if (this.paused)
|
|
168
165
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
|
|
169
166
|
if (this.sbx)
|
|
170
167
|
return ok(this.workspaceHandle());
|
|
171
168
|
if (this.connecting)
|
|
172
|
-
return this.connecting;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
return this.connecting.promise;
|
|
170
|
+
const mine = { target: undefined, promise: this.doConnect(config) };
|
|
171
|
+
this.connecting = mine;
|
|
172
|
+
const r = await mine.promise;
|
|
173
|
+
if (!r.ok && this.connecting === mine)
|
|
176
174
|
this.connecting = undefined;
|
|
177
175
|
return r;
|
|
178
176
|
}
|
|
@@ -201,6 +199,14 @@ export class RemoteContainerExecutionEnv {
|
|
|
201
199
|
await sbx.kill().catch(() => { });
|
|
202
200
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
203
201
|
}
|
|
202
|
+
const filled = await this.fillAt(sbx, this.cfg.mountPath, "minted");
|
|
203
|
+
if (!filled.ok || this.destroyed) {
|
|
204
|
+
await sbx.kill().catch(() => { });
|
|
205
|
+
this.cfg.metrics?.inc("remote_env_connect_total", { provider: PROVIDER, result: "error" });
|
|
206
|
+
if (!filled.ok && filled.standing)
|
|
207
|
+
this.connectRefusal = filled.error;
|
|
208
|
+
return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
|
|
209
|
+
}
|
|
204
210
|
this.sbx = sbx;
|
|
205
211
|
this.paused = false;
|
|
206
212
|
this.startKeepAlive();
|
|
@@ -250,20 +256,50 @@ export class RemoteContainerExecutionEnv {
|
|
|
250
256
|
return { ok: false, error: this.remoteError(e, options?.abortSignal?.aborted ? "aborted" : "unknown") };
|
|
251
257
|
}
|
|
252
258
|
}
|
|
253
|
-
|
|
259
|
+
resumeVM(snapshotId, options) {
|
|
254
260
|
if (this.destroyed)
|
|
255
|
-
return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
|
|
256
|
-
if (this.
|
|
257
|
-
return { ok: false, error: this.
|
|
261
|
+
return Promise.resolve({ ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") });
|
|
262
|
+
if (this.connectRefusal)
|
|
263
|
+
return Promise.resolve({ ok: false, error: this.connectRefusal });
|
|
258
264
|
if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
|
|
259
|
-
return ok(this.workspaceHandle());
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
return Promise.resolve(ok(this.workspaceHandle()));
|
|
266
|
+
const inFlight = this.connecting;
|
|
267
|
+
if (inFlight?.target === snapshotId)
|
|
268
|
+
return inFlight.promise;
|
|
269
|
+
const mine = { target: snapshotId, promise: this.resumeWalk(inFlight, snapshotId, options) };
|
|
270
|
+
this.connecting = mine;
|
|
271
|
+
const forget = () => {
|
|
272
|
+
if (this.connecting === mine)
|
|
273
|
+
this.connecting = undefined;
|
|
274
|
+
};
|
|
275
|
+
mine.promise.then((r) => {
|
|
276
|
+
if (!r.ok)
|
|
277
|
+
forget();
|
|
278
|
+
}, forget);
|
|
279
|
+
return mine.promise;
|
|
280
|
+
}
|
|
281
|
+
async resumeWalk(inFlight, snapshotId, options) {
|
|
282
|
+
if (inFlight !== undefined) {
|
|
283
|
+
await inFlight.promise;
|
|
284
|
+
if (this.destroyed)
|
|
285
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
|
|
286
|
+
if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
|
|
287
|
+
return ok(this.workspaceHandle());
|
|
288
|
+
}
|
|
289
|
+
if (this.sbx && !this.paused)
|
|
290
|
+
await this.releaseMinted(snapshotId);
|
|
291
|
+
return this.doResumeVM(snapshotId, options);
|
|
292
|
+
}
|
|
293
|
+
async releaseMinted(resumed) {
|
|
294
|
+
const sbx = this.sbx;
|
|
295
|
+
if (sbx === undefined)
|
|
296
|
+
return;
|
|
297
|
+
this.sbx = undefined;
|
|
298
|
+
this.handle = undefined;
|
|
299
|
+
this.stopKeepAlive();
|
|
300
|
+
await this.disposeBackgroundShells();
|
|
301
|
+
await this.rpc("sandbox.kill", () => sbx.kill()).catch((e) => this.cfg.logger?.warn("remote_env_minted_release_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
|
|
302
|
+
this.cfg.logger?.info?.("remote_env_minted_released", { sandboxId: sbx.sandboxId, resumed });
|
|
267
303
|
}
|
|
268
304
|
async doResumeVM(snapshotId, options) {
|
|
269
305
|
try {
|
|
@@ -288,6 +324,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
288
324
|
this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
|
|
289
325
|
return { ok: false, error: home };
|
|
290
326
|
}
|
|
327
|
+
const filled = await this.fillAt(sbx, restoredRoot, "restored");
|
|
328
|
+
if (this.destroyed) {
|
|
329
|
+
await sbx.kill().catch(() => { });
|
|
330
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
|
|
331
|
+
}
|
|
332
|
+
if (!filled.ok) {
|
|
333
|
+
await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), undefined, this.cfg.dataTimeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_fill_refused_repause_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
|
|
334
|
+
this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
|
|
335
|
+
if (filled.standing)
|
|
336
|
+
this.connectRefusal = filled.error;
|
|
337
|
+
return { ok: false, error: filled.error };
|
|
338
|
+
}
|
|
291
339
|
this.sbx = sbx;
|
|
292
340
|
this.paused = false;
|
|
293
341
|
this.startKeepAlive();
|
|
@@ -340,9 +388,39 @@ export class RemoteContainerExecutionEnv {
|
|
|
340
388
|
return undefined;
|
|
341
389
|
const error = new RemoteExecutionError("connect_failed", verdict.refusal);
|
|
342
390
|
if (verdict.answered)
|
|
343
|
-
this.
|
|
391
|
+
this.connectRefusal = error;
|
|
344
392
|
return error;
|
|
345
393
|
}
|
|
394
|
+
fillAt(sbx, root, origin) {
|
|
395
|
+
return this.cfg.workspaceFill === undefined ? Promise.resolve({ ok: true }) : this.cfg.workspaceFill.fill(this.fillChannel(sbx), root, origin);
|
|
396
|
+
}
|
|
397
|
+
fillChannel(sbx) {
|
|
398
|
+
return {
|
|
399
|
+
run: async (command) => {
|
|
400
|
+
const s = this.startIn(ADAPTER_START, command, undefined);
|
|
401
|
+
if (!s.ok)
|
|
402
|
+
return { ok: false, error: s.error.message };
|
|
403
|
+
try {
|
|
404
|
+
const r = await this.rpc("workspace.fill", (signal) => this.runIn(sbx, s.value, { signal, timeoutMs: this.cfg.dataTimeoutMs }), undefined, this.cfg.dataTimeoutMs);
|
|
405
|
+
return { ok: true, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
if (e instanceof CommandExitError)
|
|
409
|
+
return { ok: true, exitCode: e.exitCode, stdout: e.stdout, stderr: e.stderr };
|
|
410
|
+
return { ok: false, error: errMsg(e) };
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
write: async (abs, bytes) => {
|
|
414
|
+
try {
|
|
415
|
+
await this.rpc("files.write", (signal) => sbx.files.write(abs, toArrayBuffer(bytes), { signal }), undefined, this.cfg.dataTimeoutMs);
|
|
416
|
+
return { ok: true };
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
return { ok: false, error: errMsg(e) };
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
346
424
|
startKeepAlive() {
|
|
347
425
|
this.stopKeepAlive();
|
|
348
426
|
const everyMs = Math.max(30_000, Math.floor(this.cfg.timeoutMs * 0.8));
|
|
@@ -1130,10 +1208,15 @@ export class RemoteContainerExecutionEnv {
|
|
|
1130
1208
|
}
|
|
1131
1209
|
}
|
|
1132
1210
|
export function e2bExecutionEnvFactory(config, deps) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1211
|
+
const { workspaceFillOf, ...base } = config;
|
|
1212
|
+
return (ctx) => {
|
|
1213
|
+
const workspaceFill = workspaceFillOf?.(ctx);
|
|
1214
|
+
return new RemoteContainerExecutionEnv({
|
|
1215
|
+
...base,
|
|
1216
|
+
metadata: { ...base.metadata, semaManaged: "1", sessionId: ctx.sessionId, ...(ctx.taskId ? { taskId: ctx.taskId } : {}) },
|
|
1217
|
+
...(workspaceFill !== undefined ? { workspaceFill } : {}),
|
|
1218
|
+
}, deps);
|
|
1219
|
+
};
|
|
1137
1220
|
}
|
|
1138
1221
|
function errMsg(e) {
|
|
1139
1222
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -123,6 +123,9 @@ export interface K8sEnvConfig extends LaneHomeConfig {
|
|
|
123
123
|
/** Run-end outcome seat — called once per `destroy()` that attempted a space snapshot (the factory's caller binds it
|
|
124
124
|
* to the run, so the run's terminal frame can carry `workspaceSnapshot: "failed"`). */
|
|
125
125
|
onWorkspaceSnapshot?: (outcome: "saved" | "failed") => void;
|
|
126
|
+
/** 恢复点的填充座(`workspace-fill.ts`;工厂按 run 装配):pod 的工作区铸出 / 恢复之后、`connect` / `resumeVM` 交还之前,经 {@link RemoteK8sExecutionEnv} 自己的
|
|
127
|
+
* exec 通道(不经公开面 —— 连接进行中公开面会等同一只连接记忆)往工作区里放内容。缺席 ⇒ 不放。 */
|
|
128
|
+
workspaceFill?: WorkspaceFill;
|
|
126
129
|
/**
|
|
127
130
|
* 🔴 ADDITIVE pod-spec patch (a SERVICE-side adapter seam, NOT a core seam: the K8s pod shape is
|
|
128
131
|
* provider-specific, so it stays in the adapter per [ref]). A deployment supplies extra volumes / volumeMounts
|
|
@@ -171,6 +174,7 @@ import { type SandboxSecretSeatKind, type SandboxSecretVerb } from "./k8s-exec-p
|
|
|
171
174
|
export { type PodSpecPatch, applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
172
175
|
import { type SandboxPodApi, type SandboxPodListing } from "./k8s-pod-reaper.js";
|
|
173
176
|
import { type SandboxLeaseEntryInput } from "../sandbox-git-lease.js";
|
|
177
|
+
import type { WorkspaceFill } from "./workspace-fill.js";
|
|
174
178
|
type ConnectResult = {
|
|
175
179
|
ok: true;
|
|
176
180
|
value: WorkspaceHandle;
|
|
@@ -318,9 +322,12 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
318
322
|
private readonly inFlight;
|
|
319
323
|
/** Resolvers of {@link whenIdle}: woken when the last command in flight ends. */
|
|
320
324
|
private idleWaiters;
|
|
321
|
-
/** This env's one
|
|
322
|
-
*
|
|
325
|
+
/** This env's one lifecycle walk in flight — a connect ({@link connect}) or a resume ({@link resumeVM}): the explicit connect, the lazy connect of
|
|
326
|
+
* the first operation and `reconnect` that arrive while it runs share its answer — one pod creation, one restore, one fill (the E2B race lesson).
|
|
327
|
+
* Cleared the moment it settles, whatever the outcome (by the walk that set it, never by an earlier one). */
|
|
323
328
|
private connectInFlight?;
|
|
329
|
+
/** The resume among them, with the snapshot it restores: a second `resumeVM` of the same snapshot shares it (one restore, one pod). */
|
|
330
|
+
private resumeInFlight?;
|
|
324
331
|
/** The env's refusal — a fact of this env until `destroy`, written once ({@link holdRefusal}) and answered as it stands to every later connect (and to
|
|
325
332
|
* every later pod creation: {@link provision}): the same refusal, zero pods, zero store requests, no second line. Two causes write it:
|
|
326
333
|
* the space restore (`latest` names an archive that is there and did not come back — {@link restoreSnapshot}) and the pod's admission (the cluster created
|
|
@@ -355,6 +362,7 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
355
362
|
private cacheUnavailableSaid;
|
|
356
363
|
private readonly logger?;
|
|
357
364
|
private readonly onWorkspaceSnapshot?;
|
|
365
|
+
private readonly workspaceFill?;
|
|
358
366
|
private readonly cacheMisses?;
|
|
359
367
|
constructor(config: K8sEnvConfig, deps?: K8sEnvDeps);
|
|
360
368
|
/** The lease seat of the pod this env holds now — `undefined` without a broker seat or without a pod (the server's lease writes follow it: a resumed env holds a new pod and a new seat). */
|
|
@@ -391,6 +399,19 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
391
399
|
connect(config?: RemoteConnectConfig): Promise<ConnectResult>;
|
|
392
400
|
/** The one walk of a connect ({@link connect} runs it once at a time). */
|
|
393
401
|
private establish;
|
|
402
|
+
/**
|
|
403
|
+
* The restore point's fill (`workspace-fill.ts`): a pod whose workspace this env just minted or restored is handed over only once the fill seat placed
|
|
404
|
+
* its content — through {@link fillChannel} (this adapter's own exec, never the public face: inside a connect the public face waits on that very connect).
|
|
405
|
+
* A fill that does not complete releases the pod (no tool and no run-end snapshot may see a half-filled workspace); a refusal that would answer the same
|
|
406
|
+
* again (`standing`) is held for the env ({@link holdRefusal}). No seat ⇒ the pod as it came. `origin` tells the seat whether the workspace was minted
|
|
407
|
+
* (a new pod with nothing restored: the space off / unavailable / without `latest`, or `latest` dangling) or restored (the space's `latest`, or a park snapshot whose archive came back; a park snapshot with no archive — a plan-only park — is minted).
|
|
408
|
+
*/
|
|
409
|
+
private settle;
|
|
410
|
+
/** The restore point's channel: a fixed command through the one start seam (start form `adapter`, the engine's commands' identity) and a write in
|
|
411
|
+
* {@link writeChunked}'s form — both straight on {@link runExec} with the data bound (never {@link ensureConnected}). */
|
|
412
|
+
private fillChannel;
|
|
413
|
+
/** Release the pod this env holds (its workspace is not handed over): the pod deleted, its seats cleared. No pod ⇒ nothing. */
|
|
414
|
+
private releasePod;
|
|
394
415
|
/** This env holds a workspace its `destroy()` will commit ⇒ arm its seat on the space's commit fence (from here the
|
|
395
416
|
* run's terminal write fences the space; `workspace-space-commit.ts`). Each write of this run's row sets its deadline to
|
|
396
417
|
* now + the upload's own wall-clock bound (`dataTimeoutMs`). No-op unless the space lane is open. */
|
|
@@ -632,7 +653,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
632
653
|
/**
|
|
633
654
|
* Resume a workspace snapshot onto a FRESH pod (resume = executionEnvFactory rebuild + resumeVM):
|
|
634
655
|
* provision a pod, then download + untar the snapshot into the workspace ({@link restoreSnapshot}, lane `park`). A
|
|
635
|
-
* missing object (
|
|
656
|
+
* missing object (a plan-only park: no pod was ever minted, so nothing was uploaded) resumes as a clean workspace whose origin is `minted` —
|
|
657
|
+
* no restorable tree — so the restore point seeds it; the resume walks the park lane and never reads the space's `latest` (which a fresh connect with the space on
|
|
658
|
+
* would restore); a snapshot that came back is `restored` and never seeded.
|
|
636
659
|
*
|
|
637
660
|
* [ref]①(priorHandle 根保真)——k8s 走**诚实臂**而非绑定臂:pod 的 workspace volume 挂在**当前**
|
|
638
661
|
* 配置根(cfg.mountPath),往 priorHandle.mountPath 绑=往容器 rootfs untar 大工作区,ephemeral-storage
|
|
@@ -641,10 +664,13 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
641
664
|
* 变更窗的分歧由 core 侧 fail-closed reopen 兜底,审批不烧错根。
|
|
642
665
|
*/
|
|
643
666
|
resumeVM(snapshotId: SnapshotId, options?: VmLifecycleOptions): Promise<ConnectResult>;
|
|
667
|
+
/** The one walk of a resume ({@link resumeVM} runs it under the connect gate). */
|
|
668
|
+
private resumeWalk;
|
|
644
669
|
/**
|
|
645
670
|
* The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace.
|
|
646
|
-
* A missing archive (404) starts from the clean workspace on both lanes
|
|
647
|
-
*
|
|
671
|
+
* A missing archive (404) starts from the clean workspace on both lanes, and on both it is the same fact — no restorable tree ⇒ origin
|
|
672
|
+
* `minted` (the restore point seeds it, exactly as on a fresh connect). What differs is only what it means:
|
|
673
|
+
* - park: a plan-only suspend has no object (the park came before anything touched the hand) — nothing to say;
|
|
648
674
|
* - space: `latest` dangles (its pointer is only written after a successful upload, so the archive was removed later —
|
|
649
675
|
* by the bucket's lifecycle rule or a hand): the data is gone by an operator rule ⇒ loud and continuable —
|
|
650
676
|
* {@link dropDanglingLatest} (one `workspace_latest_dangling` line, the pointer deleted); the run's own run-end
|
|
@@ -48,6 +48,9 @@ export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
|
48
48
|
import { sandboxRunEndHoldMs } from "./k8s-pod-reaper.js";
|
|
49
49
|
import { BROKER_COMMAND, BROKER_CONTAINER_NAME, BROKER_LEASE_DIR, BROKER_LEASE_KEY, BROKER_PROBE_ARG, BROKER_VOLUME_NAME } from "../broker/constants.js";
|
|
50
50
|
import { buildSandboxLeaseFile, placeholderLeaseEntries, sandboxGitRouteEnv } from "../sandbox-git-lease.js";
|
|
51
|
+
function minted(c) {
|
|
52
|
+
return c.ok ? { ...c, origin: "minted" } : c;
|
|
53
|
+
}
|
|
51
54
|
const CACHE_VOLUME = ADAPTER_VOLUME_NAMES.cache;
|
|
52
55
|
function occupantKeyOf(occupant) {
|
|
53
56
|
if (occupant === undefined)
|
|
@@ -213,6 +216,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
213
216
|
inFlight = new Set();
|
|
214
217
|
idleWaiters = [];
|
|
215
218
|
connectInFlight;
|
|
219
|
+
resumeInFlight;
|
|
216
220
|
connectRefusal;
|
|
217
221
|
get homeDir() {
|
|
218
222
|
return this.homeSeat.home;
|
|
@@ -231,6 +235,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
231
235
|
cacheUnavailableSaid = false;
|
|
232
236
|
logger;
|
|
233
237
|
onWorkspaceSnapshot;
|
|
238
|
+
workspaceFill;
|
|
234
239
|
cacheMisses;
|
|
235
240
|
constructor(config, deps) {
|
|
236
241
|
this.cfg = {
|
|
@@ -270,6 +275,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
270
275
|
this.cacheLane = cacheLaneOf(config.cache, config.spaceOccupant);
|
|
271
276
|
this.logger = config.logger;
|
|
272
277
|
this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
|
|
278
|
+
this.workspaceFill = config.workspaceFill;
|
|
273
279
|
this.cacheMisses = config.cacheMisses;
|
|
274
280
|
this.floor = this.deps.readFloor ?? createSandboxReadFloor({ lane: PROVIDER, knob: config.sandboxReadFloor, ...(config.logger !== undefined ? { logger: config.logger } : {}) });
|
|
275
281
|
this.capabilities = capabilitiesFor({ remoteExec: { provider: PROVIDER, s3Snapshot: this.cfg.s3Snapshot }, sandboxReadFloor: config.sandboxReadFloor });
|
|
@@ -374,10 +380,14 @@ export class RemoteK8sExecutionEnv {
|
|
|
374
380
|
});
|
|
375
381
|
}
|
|
376
382
|
connect(config) {
|
|
377
|
-
this.connectInFlight
|
|
378
|
-
this.connectInFlight
|
|
383
|
+
if (this.connectInFlight)
|
|
384
|
+
return this.connectInFlight;
|
|
385
|
+
const walk = this.establish(config).finally(() => {
|
|
386
|
+
if (this.connectInFlight === walk)
|
|
387
|
+
this.connectInFlight = undefined;
|
|
379
388
|
});
|
|
380
|
-
|
|
389
|
+
this.connectInFlight = walk;
|
|
390
|
+
return walk;
|
|
381
391
|
}
|
|
382
392
|
async establish(config) {
|
|
383
393
|
if (this.connectRefusal)
|
|
@@ -386,10 +396,10 @@ export class RemoteK8sExecutionEnv {
|
|
|
386
396
|
const lane = this.spaceLane;
|
|
387
397
|
switch (lane.state) {
|
|
388
398
|
case "off":
|
|
389
|
-
return this.provision(signal);
|
|
399
|
+
return this.settle(minted(await this.provision(signal)));
|
|
390
400
|
case "unavailable":
|
|
391
401
|
this.sayUnavailable(lane.reason);
|
|
392
|
-
return this.provision(signal);
|
|
402
|
+
return this.settle(minted(await this.provision(signal)));
|
|
393
403
|
case "unreadable":
|
|
394
404
|
return this.provision(signal);
|
|
395
405
|
case "open": {
|
|
@@ -399,7 +409,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
399
409
|
const latest = await this.readLatest(lane.space, signal);
|
|
400
410
|
if (!latest.ok)
|
|
401
411
|
return latest;
|
|
402
|
-
const c = latest.value === undefined ? await this.provision(signal) : await this.restoreSnapshot({ lane: "space", space: lane.space }, latest.value, signal);
|
|
412
|
+
const c = await this.settle(latest.value === undefined ? minted(await this.provision(signal)) : await this.restoreSnapshot({ lane: "space", space: lane.space }, latest.value, signal));
|
|
403
413
|
if (c.ok)
|
|
404
414
|
this.armSpace();
|
|
405
415
|
return c;
|
|
@@ -410,6 +420,43 @@ export class RemoteK8sExecutionEnv {
|
|
|
410
420
|
}
|
|
411
421
|
}
|
|
412
422
|
}
|
|
423
|
+
async settle(c) {
|
|
424
|
+
if (!c.ok)
|
|
425
|
+
return c;
|
|
426
|
+
const fill = this.workspaceFill;
|
|
427
|
+
if (fill !== undefined) {
|
|
428
|
+
const filled = await fill.fill(this.fillChannel(), this.cwd, c.origin);
|
|
429
|
+
if (!filled.ok) {
|
|
430
|
+
await this.releasePod();
|
|
431
|
+
return filled.standing ? this.holdRefusal(filled.error) : { ok: false, error: filled.error };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (this.destroyed) {
|
|
435
|
+
await this.releasePod();
|
|
436
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", "env destroyed during connect") };
|
|
437
|
+
}
|
|
438
|
+
return { ok: true, value: c.value };
|
|
439
|
+
}
|
|
440
|
+
fillChannel() {
|
|
441
|
+
const exec = (command) => this.runExec(ADAPTER_START, command, { capMs: this.cfg.dataTimeoutMs });
|
|
442
|
+
return {
|
|
443
|
+
run: async (command) => {
|
|
444
|
+
const r = await exec(command);
|
|
445
|
+
return r.ok ? { ok: true, exitCode: r.value.exitCode, stdout: r.value.stdout, stderr: r.value.stderr } : { ok: false, error: r.error.message };
|
|
446
|
+
},
|
|
447
|
+
write: async (abs, bytes) => {
|
|
448
|
+
const w = await this.writeChunked(abs, bytes, true, undefined, exec);
|
|
449
|
+
return w.ok ? { ok: true } : { ok: false, error: w.error.message };
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async releasePod() {
|
|
454
|
+
const name = this.podName;
|
|
455
|
+
this.podName = undefined;
|
|
456
|
+
this.podIP = undefined;
|
|
457
|
+
if (name)
|
|
458
|
+
await this.deletePod(name).catch(() => { });
|
|
459
|
+
}
|
|
413
460
|
armSpace() {
|
|
414
461
|
if (this.spaceLane.state === "open")
|
|
415
462
|
this.spaceLane.space.fence.arm(this.cfg.dataTimeoutMs);
|
|
@@ -1202,10 +1249,31 @@ export class RemoteK8sExecutionEnv {
|
|
|
1202
1249
|
await this.deletePod(name).catch(() => { });
|
|
1203
1250
|
return ok(snapshotId);
|
|
1204
1251
|
}
|
|
1205
|
-
|
|
1252
|
+
resumeVM(snapshotId, options) {
|
|
1206
1253
|
if (!this.cfg.s3Snapshot)
|
|
1207
|
-
return unsupported("resumeVM");
|
|
1208
|
-
const
|
|
1254
|
+
return Promise.resolve(unsupported("resumeVM"));
|
|
1255
|
+
const same = this.resumeInFlight;
|
|
1256
|
+
if (same?.snapshotId === snapshotId)
|
|
1257
|
+
return same.walk;
|
|
1258
|
+
const prior = this.connectInFlight;
|
|
1259
|
+
const walk = this.resumeWalk(prior, snapshotId, options).finally(() => {
|
|
1260
|
+
if (this.connectInFlight === walk)
|
|
1261
|
+
this.connectInFlight = undefined;
|
|
1262
|
+
if (this.resumeInFlight?.walk === walk)
|
|
1263
|
+
this.resumeInFlight = undefined;
|
|
1264
|
+
});
|
|
1265
|
+
this.connectInFlight = walk;
|
|
1266
|
+
this.resumeInFlight = { snapshotId, walk };
|
|
1267
|
+
return walk;
|
|
1268
|
+
}
|
|
1269
|
+
async resumeWalk(prior, snapshotId, options) {
|
|
1270
|
+
if (prior)
|
|
1271
|
+
await prior;
|
|
1272
|
+
if (this.podName) {
|
|
1273
|
+
this.logger?.warn("sandbox_pod_released_before_restore", { pod: this.podName, ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}), effect: "this env held a pod minted before the park snapshot's restore — it is deleted; the restore provisions the leg's one pod" });
|
|
1274
|
+
await this.releasePod();
|
|
1275
|
+
}
|
|
1276
|
+
const r = await this.settle(await this.restoreSnapshot({ lane: "park" }, snapshotId, options?.abortSignal));
|
|
1209
1277
|
if (!r.ok)
|
|
1210
1278
|
return r;
|
|
1211
1279
|
this.handleSnapshotId = snapshotId;
|
|
@@ -1234,15 +1302,14 @@ export class RemoteK8sExecutionEnv {
|
|
|
1234
1302
|
? { cls: restoreClassOfExit(r.value.exitCode), detail: `workspace snapshot restore exited ${r.value.exitCode}: ${this.redactUrl(r.value.stderr, url).slice(0, 300)}` }
|
|
1235
1303
|
: undefined;
|
|
1236
1304
|
if (!failure) {
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1305
|
+
if (r.ok && r.value.stdout.includes("EMPTY_SNAPSHOT")) {
|
|
1306
|
+
if (target.lane === "space")
|
|
1307
|
+
await this.dropDanglingLatest(target.space, snapshotId);
|
|
1308
|
+
return { ok: true, value: this.workspaceHandle(), origin: "minted" };
|
|
1309
|
+
}
|
|
1310
|
+
return { ok: true, value: this.workspaceHandle(), origin: "restored" };
|
|
1240
1311
|
}
|
|
1241
|
-
|
|
1242
|
-
this.podName = undefined;
|
|
1243
|
-
this.podIP = undefined;
|
|
1244
|
-
if (name)
|
|
1245
|
-
await this.deletePod(name).catch(() => { });
|
|
1312
|
+
await this.releasePod();
|
|
1246
1313
|
if (attempt < RESTORE_MAX_ATTEMPTS[target.lane] && restoreFailureIsTransient(failure.cls)) {
|
|
1247
1314
|
if (await this.restorePause(abortSignal))
|
|
1248
1315
|
continue;
|
|
@@ -2081,12 +2148,12 @@ export class RemoteK8sExecutionEnv {
|
|
|
2081
2148
|
const abs = this.resolve(p);
|
|
2082
2149
|
return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => ctx.inPlace((signal) => this.writeChunked(abs, content, false, signal)));
|
|
2083
2150
|
}
|
|
2084
|
-
async writeChunked(abs, content, truncateFirst, abortSignal) {
|
|
2151
|
+
async writeChunked(abs, content, truncateFirst, abortSignal, exec = (command) => this.execData(command, abortSignal)) {
|
|
2085
2152
|
const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
2086
2153
|
for (let off = 0, first = true; off < bytes.length || first; off += WRITE_CHUNK_BYTES, first = false) {
|
|
2087
2154
|
const b64 = bytes.subarray(off, off + WRITE_CHUNK_BYTES).toString("base64");
|
|
2088
2155
|
const redirect = first && truncateFirst ? ">" : ">>";
|
|
2089
|
-
const r = await
|
|
2156
|
+
const r = await exec(`printf '%s' ${shellQuote(b64)} | base64 -d ${redirect} ${shellQuote(abs)}`);
|
|
2090
2157
|
if (!r.ok)
|
|
2091
2158
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
2092
2159
|
if (r.value.exitCode !== 0)
|
|
@@ -3,6 +3,7 @@ import { type LaneHomeConfig } from "../lane-home-probe.js";
|
|
|
3
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
4
|
import { type SandboxReadFloorKnob } from "../execution-lane-caps.js";
|
|
5
5
|
import { type SandboxReadFloor } from "./sandbox-read-floor.js";
|
|
6
|
+
import type { WorkspaceFill, WorkspaceFillOf } from "./workspace-fill.js";
|
|
6
7
|
/** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
|
|
7
8
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
8
9
|
/** Construction config for {@link RemoteLocalDockerExecutionEnv}. */
|
|
@@ -59,6 +60,8 @@ export interface LocalDockerEnvConfig extends LaneHomeConfig {
|
|
|
59
60
|
id?: string;
|
|
60
61
|
/** `SANDBOX_READ_FLOOR`(keep | off;缺席 ⇒ keep):本容器守不守读拒表的字节地板 = 读地板模块服务本车道 ∧ 本键不是 off(`sandbox-read-floor.ts`)。 */
|
|
61
62
|
sandboxReadFloor?: SandboxReadFloorKnob;
|
|
63
|
+
/** 恢复点的填充座(`workspace-fill.ts`;工厂按 run 装配):容器新建之后、交给本环境之前,经本适配器自己的 `docker exec` 往工作区里放内容。缺席 ⇒ 不放。 */
|
|
64
|
+
workspaceFill?: WorkspaceFill;
|
|
62
65
|
}
|
|
63
66
|
/** Inject `spawn` for tests (a fake docker CLI). Defaults to node's child_process spawn. */
|
|
64
67
|
export interface LocalDockerEnvDeps {
|
|
@@ -92,12 +95,15 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
92
95
|
private destroyed;
|
|
93
96
|
/** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */
|
|
94
97
|
private connecting?;
|
|
95
|
-
/**
|
|
96
|
-
|
|
98
|
+
/** 这只环境的拒 —— 记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。两个写者:家目录(环境答了而与声明不一致)
|
|
99
|
+
* 与恢复点的填充(换一只容器也是同一个答的拒:种子不是项目包、摘要不符、镜像没有 `tar`、镜像预置撞名 —— `FillOutcome.standing`)。 */
|
|
100
|
+
private connectRefusal?;
|
|
97
101
|
/** 家目录的座(声明在场 ⇒ 开环境那一拍对证 · 否则开容器那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。与其余读得出家目录的车道同一只判官、同一句拒。 */
|
|
98
102
|
private readonly homeSeat;
|
|
99
103
|
constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps);
|
|
100
104
|
private resolve;
|
|
105
|
+
/** 收据句柄。还没连上(懒环境:续跑腿的引擎在恢复相**之前**先取它比车道)⇒ 未连接形 `{ sandboxId: "(unconnected)", provider, mountPath }`(与 k8s / e2b 车道同形),
|
|
106
|
+
* 不抛 —— 抛了,续跑腿在恢复相就失败。 */
|
|
101
107
|
workspaceHandle(): WorkspaceHandle;
|
|
102
108
|
connect(config?: RemoteConnectConfig): Promise<{
|
|
103
109
|
ok: true;
|
|
@@ -107,6 +113,10 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
107
113
|
error: RemoteExecutionError;
|
|
108
114
|
}>;
|
|
109
115
|
private doConnect;
|
|
116
|
+
/** 恢复点的填充:有座 ⇒ 经 {@link fillChannel} 放(容器恒是新起的 ⇒ `minted`;本车道不可挂起);没有 ⇒ 什么都不放。 */
|
|
117
|
+
private fillWorkspace;
|
|
118
|
+
/** 恢复点手里的通道:`docker exec` 经唯一起动接缝(起动形 `adapter`,与引擎命令同一执行用户;连接进行中,不经懒连接的文件面);写 = 容器内 `cat >` 从 stdin 收整份字节。 */
|
|
119
|
+
private fillChannel;
|
|
110
120
|
/**
|
|
111
121
|
* 家目录:座要问({@link LaneHomeSeat.asks}:声明在场 ⇒ 对证;没声明 ⇒ 实测)⇒ 在本容器里问一次 `$HOME`(与命令同一条 `docker exec` 通道、同一份 `-e` env),读数交座。
|
|
112
122
|
* 声明在场:环境答了而不一致 / 不是绝对路径 ⇒ 回拒并记在环境上(同一镜像同一答案:此后每次连接原样回它,不再起容器、不再问);没答上(exec 失败 / 超时 / 非零退出)⇒ 这一次同样不开,
|
|
@@ -258,6 +268,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
258
268
|
* owns the lifetime and calls `destroy()` (= `docker rm -f`) on task end. Folds the per-task `ctx.taskId`/
|
|
259
269
|
* `ctx.sessionId` into the container name so two concurrent tasks never share a container.
|
|
260
270
|
*/
|
|
261
|
-
export declare function localDockerExecutionEnvFactory(config: LocalDockerEnvConfig,
|
|
271
|
+
export declare function localDockerExecutionEnvFactory(config: Omit<LocalDockerEnvConfig, "workspaceFill"> & {
|
|
272
|
+
readonly workspaceFillOf?: WorkspaceFillOf;
|
|
273
|
+
}, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
|
|
262
274
|
export {};
|
|
263
275
|
//# sourceMappingURL=remote-env-local-docker.d.ts.map
|
|
@@ -37,7 +37,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
37
37
|
handle;
|
|
38
38
|
destroyed = false;
|
|
39
39
|
connecting;
|
|
40
|
-
|
|
40
|
+
connectRefusal;
|
|
41
41
|
homeSeat;
|
|
42
42
|
constructor(config, deps = {}) {
|
|
43
43
|
if (!config.image || !config.image.trim()) {
|
|
@@ -64,17 +64,15 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
64
64
|
return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
|
|
65
65
|
}
|
|
66
66
|
workspaceHandle() {
|
|
67
|
-
|
|
68
|
-
throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
|
|
69
|
-
return this.handle;
|
|
67
|
+
return this.handle ?? { sandboxId: "(unconnected)", provider: PROVIDER, mountPath: this.cfg.mountPath };
|
|
70
68
|
}
|
|
71
69
|
async connect(config) {
|
|
72
70
|
if (config?.snapshotId)
|
|
73
71
|
return unsupported("connect(snapshotId)");
|
|
74
72
|
if (this.destroyed)
|
|
75
73
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
76
|
-
if (this.
|
|
77
|
-
return { ok: false, error: this.
|
|
74
|
+
if (this.connectRefusal)
|
|
75
|
+
return { ok: false, error: this.connectRefusal };
|
|
78
76
|
if (config?.abortSignal?.aborted)
|
|
79
77
|
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
80
78
|
if (this.containerId)
|
|
@@ -139,12 +137,38 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
139
137
|
await this.forceRemove().catch(() => { });
|
|
140
138
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
141
139
|
}
|
|
140
|
+
const filled = await this.fillWorkspace();
|
|
141
|
+
if (!filled.ok || this.destroyed) {
|
|
142
|
+
await this.forceRemove().catch(() => { });
|
|
143
|
+
if (!filled.ok && filled.standing)
|
|
144
|
+
this.connectRefusal = filled.error;
|
|
145
|
+
return { ok: false, error: filled.ok ? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") : filled.error };
|
|
146
|
+
}
|
|
142
147
|
this.containerId = run.value.stdout.trim() || this.containerName;
|
|
143
148
|
this.handle = { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
144
149
|
if (config?.secrets?.length) {
|
|
145
150
|
}
|
|
146
151
|
return ok(this.handle);
|
|
147
152
|
}
|
|
153
|
+
fillWorkspace() {
|
|
154
|
+
return this.cfg.workspaceFill === undefined ? Promise.resolve({ ok: true }) : this.cfg.workspaceFill.fill(this.fillChannel(), this.cfg.mountPath, "minted");
|
|
155
|
+
}
|
|
156
|
+
fillChannel() {
|
|
157
|
+
const run = async (command, input) => {
|
|
158
|
+
const argv = this.startArgv(ADAPTER_START, command, { shell: "/bin/sh", ...(input !== undefined ? { stdin: true } : {}) });
|
|
159
|
+
if (!argv.ok)
|
|
160
|
+
return { ok: false, error: argv.error.message };
|
|
161
|
+
const r = await this.docker(argv.value, { timeoutMs: this.cfg.commandTimeoutMs, ...(input !== undefined ? { input } : {}) });
|
|
162
|
+
return r.ok ? { ok: true, exitCode: r.value.exitCode, stdout: r.value.stdout, stderr: r.value.stderr } : { ok: false, error: r.error.message };
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
run: (command) => run(command),
|
|
166
|
+
write: async (abs, bytes) => {
|
|
167
|
+
const r = await run(`cat > ${shellQuote(abs)}`, bytes);
|
|
168
|
+
return !r.ok ? r : r.exitCode === 0 ? { ok: true } : { ok: false, error: r.stderr.trim() || `exit ${r.exitCode}` };
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
148
172
|
async homeVerdict(signal) {
|
|
149
173
|
if (!this.homeSeat.asks)
|
|
150
174
|
return undefined;
|
|
@@ -157,7 +181,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
157
181
|
return undefined;
|
|
158
182
|
const error = new RemoteExecutionError("connect_failed", verdict.refusal);
|
|
159
183
|
if (verdict.answered)
|
|
160
|
-
this.
|
|
184
|
+
this.connectRefusal = error;
|
|
161
185
|
return error;
|
|
162
186
|
}
|
|
163
187
|
async suspendVM(_options) {
|
|
@@ -172,8 +196,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
172
196
|
async reconnect(sessionToken) {
|
|
173
197
|
if (this.destroyed)
|
|
174
198
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
175
|
-
if (this.
|
|
176
|
-
return { ok: false, error: this.
|
|
199
|
+
if (this.connectRefusal)
|
|
200
|
+
return { ok: false, error: this.connectRefusal };
|
|
177
201
|
if (sessionToken && sessionToken !== this.containerName) {
|
|
178
202
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `reconnect token '${String(sessionToken)}' does not match this env's container '${this.containerName}'`) };
|
|
179
203
|
}
|
|
@@ -189,8 +213,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
189
213
|
const home = this.containerId === undefined ? await this.homeVerdict() : undefined;
|
|
190
214
|
if (home !== undefined)
|
|
191
215
|
return { ok: false, error: home };
|
|
192
|
-
if (this.destroyed || this.
|
|
193
|
-
return { ok: false, error: this.
|
|
216
|
+
if (this.destroyed || this.connectRefusal)
|
|
217
|
+
return { ok: false, error: this.connectRefusal ?? new RemoteExecutionError("connect_failed", "execution env destroyed during reconnect") };
|
|
194
218
|
this.containerId = this.containerName;
|
|
195
219
|
this.handle ??= { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
196
220
|
return ok(this.handle);
|
|
@@ -682,6 +706,10 @@ function errMsg(e) {
|
|
|
682
706
|
return e instanceof Error ? e.message : String(e);
|
|
683
707
|
}
|
|
684
708
|
export function localDockerExecutionEnvFactory(config, deps = {}) {
|
|
685
|
-
|
|
709
|
+
const { workspaceFillOf, ...base } = config;
|
|
710
|
+
return (ctx) => {
|
|
711
|
+
const workspaceFill = workspaceFillOf?.(ctx);
|
|
712
|
+
return new RemoteLocalDockerExecutionEnv({ ...base, id: base.id ?? ctx.taskId ?? ctx.sessionId, ...(workspaceFill !== undefined ? { workspaceFill } : {}) }, deps);
|
|
713
|
+
};
|
|
686
714
|
}
|
|
687
715
|
//# sourceMappingURL=remote-env-local-docker.js.map
|