@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -4,9 +4,10 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { hostSubprocessEnv } from "../server-secret-env.js";
|
|
6
6
|
import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
|
|
7
|
-
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
7
|
+
import { appendFileStaged, rewriteAppend, buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace, writeFileStaged } from "./remote-shell.js";
|
|
8
8
|
import { signalNumber } from "./host-platform.js";
|
|
9
9
|
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
10
|
+
import { buildCapturedOutput, createCutError } from "../exec-cut.js";
|
|
10
11
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
11
12
|
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
12
13
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
@@ -27,7 +28,7 @@ export function isAdbTransportLost(stderr) {
|
|
|
27
28
|
return /device offline|device '.*' not found|no devices\/emulators found|connection reset|protocol fault|device disconnected/i.test(stderr);
|
|
28
29
|
}
|
|
29
30
|
export class RemoteAdbExecutionEnv {
|
|
30
|
-
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
|
|
31
|
+
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
|
|
31
32
|
cwd;
|
|
32
33
|
connected = false;
|
|
33
34
|
destroyed = false;
|
|
@@ -341,57 +342,46 @@ export class RemoteAdbExecutionEnv {
|
|
|
341
342
|
lines = lines.slice(0, options.maxLines);
|
|
342
343
|
return ok(lines);
|
|
343
344
|
}
|
|
345
|
+
stagedLane(content) {
|
|
346
|
+
return { upload: (dest, signal) => this.push(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
|
|
347
|
+
}
|
|
344
348
|
async writeFile(p, content, abortSignal) {
|
|
349
|
+
const gated = await this.fsGate(p);
|
|
350
|
+
if (gated)
|
|
351
|
+
return gated;
|
|
352
|
+
return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
|
|
353
|
+
}
|
|
354
|
+
async appendFile(p, content, abortSignal) {
|
|
345
355
|
const gated = await this.fsGate(p);
|
|
346
356
|
if (gated)
|
|
347
357
|
return gated;
|
|
348
358
|
const abs = this.resolve(p);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
354
|
-
if (mk.value.exitCode !== 0)
|
|
355
|
-
return { ok: false, error: new FileError("unknown", mk.value.stderr.trim() || `mkdir -p failed for ${dir}`, dir) };
|
|
356
|
-
}
|
|
357
|
-
let local;
|
|
359
|
+
return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => rewriteAppend((signal) => this.readBinaryFile(abs, signal), (bytes, signal) => this.push(bytes, abs, signal), content, ctx));
|
|
360
|
+
}
|
|
361
|
+
async push(content, dest, abortSignal) {
|
|
362
|
+
let localDir;
|
|
358
363
|
try {
|
|
359
|
-
|
|
364
|
+
localDir = await fs.mkdtemp(path.join(os.tmpdir(), "sema-adb-push-"));
|
|
365
|
+
const local = path.join(localDir, "data");
|
|
360
366
|
await fs.writeFile(local, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
|
|
361
|
-
const r = await this.adb(["-s", this.cfg.serial, "push", local,
|
|
367
|
+
const r = await this.adb(["-s", this.cfg.serial, "push", local, dest], { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
|
|
362
368
|
if (!r.ok)
|
|
363
|
-
return { ok: false, error: fileErrorFromExec(r.error,
|
|
369
|
+
return { ok: false, error: fileErrorFromExec(r.error, dest) };
|
|
364
370
|
const out = `${r.value.stdout} ${r.value.stderr}`;
|
|
365
371
|
if (/permission denied/i.test(out))
|
|
366
|
-
return { ok: false, error: new FileError("permission_denied", out.trim(),
|
|
372
|
+
return { ok: false, error: new FileError("permission_denied", out.trim(), dest) };
|
|
367
373
|
if (r.value.exitCode !== 0)
|
|
368
|
-
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${
|
|
374
|
+
return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${dest}`, dest) };
|
|
369
375
|
return ok(undefined);
|
|
370
376
|
}
|
|
371
377
|
catch (e) {
|
|
372
|
-
return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e),
|
|
378
|
+
return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e), dest) };
|
|
373
379
|
}
|
|
374
380
|
finally {
|
|
375
|
-
if (
|
|
376
|
-
await fs.rm(
|
|
381
|
+
if (localDir)
|
|
382
|
+
await fs.rm(localDir, { recursive: true, force: true }).catch(() => { });
|
|
377
383
|
}
|
|
378
384
|
}
|
|
379
|
-
async appendFile(p, content, abortSignal) {
|
|
380
|
-
const abs = this.resolve(p);
|
|
381
|
-
const existing = await this.readBinaryFile(abs, abortSignal);
|
|
382
|
-
let base;
|
|
383
|
-
if (existing.ok)
|
|
384
|
-
base = existing.value;
|
|
385
|
-
else if (existing.error.code === "not_found")
|
|
386
|
-
base = new Uint8Array(0);
|
|
387
|
-
else
|
|
388
|
-
return existing;
|
|
389
|
-
const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
390
|
-
const merged = new Uint8Array(base.length + add.length);
|
|
391
|
-
merged.set(base, 0);
|
|
392
|
-
merged.set(add, base.length);
|
|
393
|
-
return this.writeFile(abs, merged, abortSignal);
|
|
394
|
-
}
|
|
395
385
|
posixFsInst;
|
|
396
386
|
get posixFs() {
|
|
397
387
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
@@ -451,23 +441,44 @@ export class RemoteAdbExecutionEnv {
|
|
|
451
441
|
const outBufs = [];
|
|
452
442
|
const outTail = binary ? undefined : new RollingTailBuffer();
|
|
453
443
|
const errBuf = new RollingTailBuffer();
|
|
454
|
-
const
|
|
444
|
+
const recordStderr = (bytes) => {
|
|
445
|
+
if (bytes.length > 0)
|
|
446
|
+
errBuf.push(bytes);
|
|
447
|
+
};
|
|
448
|
+
const emitStderr = (bytes) => {
|
|
449
|
+
if (bytes.length > 0)
|
|
450
|
+
opts?.onStderr?.(bytes.toString());
|
|
451
|
+
};
|
|
452
|
+
const takeStderr = (bytes) => {
|
|
453
|
+
recordStderr(bytes);
|
|
454
|
+
emitStderr(bytes);
|
|
455
|
+
};
|
|
456
|
+
const heldStderr = () => opts?.stderrFilter?.endStderr();
|
|
457
|
+
const captured = () => {
|
|
458
|
+
const held = heldStderr();
|
|
459
|
+
if (held)
|
|
460
|
+
recordStderr(held);
|
|
461
|
+
return buildCapturedOutput(outTail, errBuf);
|
|
462
|
+
};
|
|
463
|
+
const finish = (outcome) => {
|
|
455
464
|
if (settled)
|
|
456
|
-
return;
|
|
465
|
+
return false;
|
|
457
466
|
settled = true;
|
|
467
|
+
const r = outcome();
|
|
458
468
|
clearTimeout(timer);
|
|
459
469
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
460
470
|
resolve(r);
|
|
471
|
+
return true;
|
|
461
472
|
};
|
|
462
473
|
const timeoutMs = opts?.timeoutMs ?? this.cfg.commandTimeoutMs;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
killGrace();
|
|
469
|
-
finish({ ok: false, error: new ExecutionError("aborted", "adb command aborted") });
|
|
474
|
+
const cut = (c) => {
|
|
475
|
+
finish(() => {
|
|
476
|
+
killGrace();
|
|
477
|
+
return { ok: false, error: createCutError(c, captured()) };
|
|
478
|
+
});
|
|
470
479
|
};
|
|
480
|
+
const timer = setTimeout(() => cut({ code: "timeout", message: `adb ${args[0]} exceeded ${timeoutMs}ms` }), timeoutMs);
|
|
481
|
+
const onAbort = () => cut({ code: "aborted", message: "adb command aborted" });
|
|
471
482
|
if (opts?.signal) {
|
|
472
483
|
if (opts.signal.aborted)
|
|
473
484
|
return onAbort();
|
|
@@ -480,30 +491,27 @@ export class RemoteAdbExecutionEnv {
|
|
|
480
491
|
outTail.push(d);
|
|
481
492
|
opts?.onStdout?.(d.toString());
|
|
482
493
|
});
|
|
483
|
-
const takeStderr = (bytes) => {
|
|
484
|
-
if (bytes.length === 0)
|
|
485
|
-
return;
|
|
486
|
-
errBuf.push(bytes);
|
|
487
|
-
opts?.onStderr?.(bytes.toString());
|
|
488
|
-
};
|
|
489
494
|
child.stderr.on("data", (d) => takeStderr(opts?.stderrFilter ? opts.stderrFilter.filterStderr(d) : d));
|
|
490
|
-
child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) }));
|
|
495
|
+
child.on("error", (e) => finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) })));
|
|
491
496
|
child.on("close", (code, signal) => {
|
|
492
497
|
if (forceSettleTimer)
|
|
493
498
|
clearTimeout(forceSettleTimer);
|
|
494
|
-
if (opts?.stderrFilter)
|
|
495
|
-
takeStderr(opts.stderrFilter.endStderr());
|
|
496
|
-
const err = errBuf.result();
|
|
497
|
-
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
498
499
|
const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
500
|
+
const held = heldStderr();
|
|
501
|
+
const won = finish(() => {
|
|
502
|
+
if (held)
|
|
503
|
+
recordStderr(held);
|
|
504
|
+
const err = errBuf.result();
|
|
505
|
+
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
506
|
+
if (binary) {
|
|
507
|
+
const stdoutBytes = Buffer.concat(outBufs);
|
|
508
|
+
return ok({ stdout: stdoutBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(stdoutBytes) });
|
|
509
|
+
}
|
|
504
510
|
const out = outTail.result();
|
|
505
|
-
|
|
506
|
-
}
|
|
511
|
+
return ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode });
|
|
512
|
+
});
|
|
513
|
+
if (won && held)
|
|
514
|
+
emitStderr(held);
|
|
507
515
|
});
|
|
508
516
|
});
|
|
509
517
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ExecutionError, FileError, RemoteExecutionError, type ExecutionEnv, type ExecutionEnvFactory, type ExecStreamOptions, type FileInfo, type OutputChunk, type RemoteConnectConfig, type RemoteExecutionEnv, type Result, type SessionToken, type SnapshotId, type VmLifecycleOptions, type WorkspaceHandle, type WriteExpectation, type WriteReceipt } from "@sema-agent/core";
|
|
2
2
|
import { type DeviceOwner, type DeviceRejectCode, type DeviceRow, type DeviceStore } from "../device-store.js";
|
|
3
3
|
import type { DeviceEnrollment } from "../device-enrollment.js";
|
|
4
|
+
import { type ExecutionPlacementSeat } from "../execution-placement.js";
|
|
4
5
|
import type { DeviceWsHub } from "../device-ws-hub.js";
|
|
5
6
|
import type { DeviceInstructionKind, DeviceOutcomeErrorCode } from "../device-ws-protocol.js";
|
|
6
7
|
/** `WorkspaceHandle.provider` 与 `REMOTE_EXEC` 词表的那一个词,单点。 */
|
|
@@ -57,6 +58,8 @@ export interface DeviceEnvFactoryDeps {
|
|
|
57
58
|
homeDir?: string;
|
|
58
59
|
hub: DeviceWsHub;
|
|
59
60
|
store: DeviceStore;
|
|
61
|
+
/** 会话店的执行席(绑定的唯一载体与唯一读;装配点交会话店的 `executionPlacement`,与准入链同一只)。工厂只调它的 `read(root)`。 */
|
|
62
|
+
seat: Pick<ExecutionPlacementSeat, "read">;
|
|
60
63
|
enrollment: DeviceEnrollment;
|
|
61
64
|
/** 指令级默认超时(ms;`DEVICE_EXEC_TIMEOUT_MS`,§4.7)。core 的 per-command `timeout` 可收紧它。 */
|
|
62
65
|
execTimeoutMs?: number;
|
|
@@ -109,15 +112,17 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
|
109
112
|
* 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
|
|
110
113
|
*/
|
|
111
114
|
/** `sharesProcessTable`(core 7.57.0,必填):命令跑在注册设备上(设备执行器),不在本进程所在的进程表。 */
|
|
115
|
+
/** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
|
|
112
116
|
readonly capabilities: {
|
|
113
|
-
isolation:
|
|
114
|
-
suspendable:
|
|
115
|
-
canonicalPathAuthoritative:
|
|
116
|
-
sharesProcessTable:
|
|
117
|
+
isolation: false;
|
|
118
|
+
suspendable: false;
|
|
119
|
+
canonicalPathAuthoritative: true;
|
|
120
|
+
sharesProcessTable: false;
|
|
121
|
+
hostLocalPaths: false;
|
|
122
|
+
externalContentTarget: true;
|
|
123
|
+
io: true;
|
|
124
|
+
keepsReadFloor: false;
|
|
117
125
|
};
|
|
118
|
-
readonly externalContentTarget = true;
|
|
119
|
-
/** [ref]:设备路径不在 worker 宿主文件系统上 —— host-path 回执消费方据此拒绝把它当宿主路径呈现。 */
|
|
120
|
-
readonly hostLocalPaths = false;
|
|
121
126
|
cwd: string;
|
|
122
127
|
/** 可选面(presence-typed,见文件头「铁律」段):构造期按设备声明装配,缺席就是缺席。 */
|
|
123
128
|
readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
|
|
@@ -332,11 +337,24 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
|
|
|
332
337
|
* `DEVICE_ERROR_CODES` 闭集(装配 / 数据错误取 {@link DEVICE_FACTORY_CONFIG_CODES}:placement 根为空 · 有效工作目录非绝对路径),句子里的 `[码]` 与 `.code` 同值。修前抛裸 `Error`,码只在句子里
|
|
333
338
|
* (有效工作目录那一只直到 7.135.0 仍是裸 `Error`;钉:本工厂与 {@link effectiveDeviceCwd} 的函数体里没有 `new Error(`)。
|
|
334
339
|
*
|
|
335
|
-
* 🔴
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
+
* 🔴 **工厂认设备不认住客**(读口一只 = 会话店执行席 `seat.read(root)`,与解析器 / 换绑门 / 执行放置读面 / 准入链同一只读):按 placement 根读**当前**格,
|
|
341
|
+
* 与这条腿带来的设备句柄 `honor` 比,不同即 typed 拒。判序(一次行读):
|
|
342
|
+
* root = ctx.placementRootSessionId // 空 ⇒ config.placement_root_invalid
|
|
343
|
+
* honor = ctx.resumeWorkspace?.deviceId
|
|
344
|
+
* cell = seat.read(root).cell // 行不在 / 格缺 / mirror / writer(device, ∅) ⇒ device.not_bound(唯一读法 `boundDeviceOf`)
|
|
345
|
+
* honor ≠ undefined ∧ 格上设备 ≠ honor ⇒ device.identity_mismatch(句点名两台,零连接)
|
|
346
|
+
* readDeviceForAdmission(格上设备, 格上属主)
|
|
347
|
+
* 工厂**不**读 `task_run`、不读子代分区行、不按 `ctx.taskId`、不比化身、不比属主(住客谓词只用于写与 judged 复核,不是工厂的读者)。
|
|
348
|
+
* 续跑腿(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备 ⇒ 本工厂**只**在那台设备上铸 —— 根此刻绑的不是它(换绑过)
|
|
349
|
+
* ⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。「绑的就是它但现在不在线」与新跑同形:
|
|
350
|
+
* 构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。行没记设备(早于该字段的行)⇒ 引擎已命名的「行未记设备 ⇒ 普通路由」臂,按根读当前格。
|
|
351
|
+
* 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
|
|
352
|
+
*
|
|
353
|
+
* 🔴 **过渡期(条件句)**:新派后代腿的放置句柄(引擎在 `ExecutionEnvFactoryContext` 上的放置工作区字段,`honor` 的第二来源)**只在本仓所基的引擎版本带它时**
|
|
354
|
+
* 进判序;本仓今天所基的引擎上下文没有这个字段 ⇒ `honor` 只取续跑腿的 `resumeWorkspace`,新派后代腿按根读当前格。本仓**不**注入任何替代见证、不造任何行
|
|
355
|
+
* (修在源头)。敞口(在那个字段到货之前成立):活过父 turn 的后台子代,在根换绑之后新派的腿会落到根当前设备 —— 换绑半边由换绑门的子代活性判关闭(归换绑门那一车),
|
|
356
|
+
* 删除半边另有票。字段到货后的形:`honor = ctx.resumeWorkspace?.deviceId ?? <放置句柄>.deviceId`,放置句柄的 provider 不是本车道 ⇒ 装配拒(`config.placement_root_invalid` 同级)。
|
|
357
|
+
* 本仓任何文本不得宣称工厂「一律按住客定址」。
|
|
340
358
|
*/
|
|
341
359
|
export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
|
|
342
360
|
/**
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
2
|
import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
|
|
3
3
|
import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
|
|
4
|
+
import { boundDeviceOf } from "../execution-placement.js";
|
|
5
|
+
import { createCutError, createExecFailure } from "../exec-cut.js";
|
|
4
6
|
export const DEVICE_PROVIDER = "device";
|
|
5
7
|
const ok = (value) => ({ ok: true, value });
|
|
6
8
|
const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -126,9 +128,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
126
128
|
homeDir;
|
|
127
129
|
cdPath = "unknown";
|
|
128
130
|
withheldEnv = [];
|
|
129
|
-
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
|
|
130
|
-
externalContentTarget = true;
|
|
131
|
-
hostLocalPaths = false;
|
|
131
|
+
capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: true, io: true, keepsReadFloor: false };
|
|
132
132
|
cwd;
|
|
133
133
|
readLink;
|
|
134
134
|
writeFileExclusive;
|
|
@@ -227,7 +227,8 @@ export class RemoteDeviceExecutionEnv {
|
|
|
227
227
|
}
|
|
228
228
|
let admitted;
|
|
229
229
|
try {
|
|
230
|
-
|
|
230
|
+
const placement = await this.cfg.enrollment.readPlacement(this.cfg.rootSessionId);
|
|
231
|
+
admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, placement, requester: this.cfg.owner });
|
|
231
232
|
}
|
|
232
233
|
catch (err) {
|
|
233
234
|
return {
|
|
@@ -302,7 +303,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
302
303
|
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
303
304
|
const failure = await this.ensureConnected();
|
|
304
305
|
if (failure)
|
|
305
|
-
return { ok: false, error:
|
|
306
|
+
return { ok: false, error: createExecFailure(failure.exec, failure.message) };
|
|
306
307
|
return undefined;
|
|
307
308
|
}
|
|
308
309
|
async fsGate(p) {
|
|
@@ -381,15 +382,14 @@ export class RemoteDeviceExecutionEnv {
|
|
|
381
382
|
case "env_terminal":
|
|
382
383
|
return { ok: false, error: new ExecutionError("shell_unavailable", ENV_TERMINAL_MESSAGE) };
|
|
383
384
|
case "rejected":
|
|
384
|
-
return { ok: false, error:
|
|
385
|
+
return { ok: false, error: createExecFailure(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
|
|
385
386
|
case "never_started":
|
|
386
387
|
case "settled": {
|
|
387
388
|
const o = res.outcome;
|
|
388
|
-
if (!o.ok)
|
|
389
|
-
return { ok: false, error:
|
|
390
|
-
}
|
|
389
|
+
if (!o.ok)
|
|
390
|
+
return { ok: false, error: createExecFailure(DEVICE_OUTCOME_TO_EXEC_ERROR[o.errorCode], o.message, res.kind === "settled" ? { stdout, stderr } : undefined) };
|
|
391
391
|
if (o.kind === "cancelled") {
|
|
392
|
-
return { ok: false, error:
|
|
392
|
+
return { ok: false, error: createCutError({ code: "aborted", message: `the device reported the command as ${o.state} after a cancel` }, { stdout, stderr }) };
|
|
393
393
|
}
|
|
394
394
|
if (o.kind !== "exec")
|
|
395
395
|
return { ok: false, error: new ExecutionError("unknown", `device returned a "${o.kind}" outcome for an exec — protocol violation`) };
|
|
@@ -799,17 +799,18 @@ export function deviceExecutionEnvFactory(deps) {
|
|
|
799
799
|
throw deviceFactoryRefusal(DEVICE_FACTORY_CONFIG_CODES.PLACEMENT_ROOT_INVALID, `device lane: ExecutionEnvFactoryContext.placementRootSessionId is empty [${DEVICE_FACTORY_CONFIG_CODES.PLACEMENT_ROOT_INVALID}] — ` +
|
|
800
800
|
"the device placement is keyed on the run tree's placement root; defaulting to the child sessionId would read as a NEW placement (v2 §4.3.2 / core design/380 O1①)");
|
|
801
801
|
}
|
|
802
|
-
const
|
|
802
|
+
const placement = await deps.seat.read(rootSessionId);
|
|
803
|
+
const binding = placement === undefined ? undefined : boundDeviceOf(placement.cell);
|
|
803
804
|
if (!binding) {
|
|
804
805
|
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_BOUND, `device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
|
|
805
806
|
"the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
|
|
806
807
|
}
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
809
|
-
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${
|
|
808
|
+
const honor = ctx.resumeWorkspace?.deviceId;
|
|
809
|
+
if (honor !== undefined && honor !== binding.id) {
|
|
810
|
+
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${honor}, but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
|
|
810
811
|
"a parked call does not follow a rebind (the new device has no copy of that workspace); finish or cancel the parked task, then start it again on the device now bound");
|
|
811
812
|
}
|
|
812
|
-
const view = await deps.store.readDeviceForAdmission(binding.
|
|
813
|
+
const view = await deps.store.readDeviceForAdmission(binding.id, binding.owner);
|
|
813
814
|
if (!view) {
|
|
814
815
|
throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_FOUND, `device lane: the bound device is gone or not owned by the binding's principal [${DEVICE_ERROR_CODES.NOT_FOUND}]`);
|
|
815
816
|
}
|
|
@@ -821,13 +822,13 @@ export function deviceExecutionEnvFactory(deps) {
|
|
|
821
822
|
hub: deps.hub,
|
|
822
823
|
enrollment: deps.enrollment,
|
|
823
824
|
rootSessionId,
|
|
824
|
-
deviceId: binding.
|
|
825
|
+
deviceId: binding.id,
|
|
825
826
|
owner: binding.owner,
|
|
826
827
|
cwd: effectiveDeviceCwd(deps.perSessionCwd?.get(ctx.sessionId), ctx.parentCwd, view.row),
|
|
827
828
|
...(deps.homeDir === undefined ? {} : { homeDir: deps.homeDir }),
|
|
828
829
|
...(deps.perSessionShellEnv?.get(ctx.sessionId) === undefined ? {} : { shellEnv: deps.perSessionShellEnv.get(ctx.sessionId) }),
|
|
829
830
|
execTimeoutMs,
|
|
830
|
-
supports: deps.hub.declaredSupports(binding.
|
|
831
|
+
supports: deps.hub.declaredSupports(binding.id),
|
|
831
832
|
...(deps.logger === undefined ? {} : { logger: deps.logger }),
|
|
832
833
|
});
|
|
833
834
|
};
|
|
@@ -136,11 +136,16 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
136
136
|
/** [ref] §9: E2B is a FULL RemoteExecutionEnv — an isolated, snapshot-suspendable microVM. (SSH/ADB
|
|
137
137
|
* adapters declare {isolation:false, suspendable:false} — real machines/devices, not snapshotable.) */
|
|
138
138
|
/** `sharesProcessTable`(core 7.57.0,必填):命令跑在云沙箱里,不在本进程所在的进程表。 */
|
|
139
|
+
/** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
|
|
139
140
|
readonly capabilities: {
|
|
140
|
-
isolation:
|
|
141
|
-
suspendable:
|
|
142
|
-
canonicalPathAuthoritative:
|
|
143
|
-
sharesProcessTable:
|
|
141
|
+
isolation: true;
|
|
142
|
+
suspendable: true;
|
|
143
|
+
canonicalPathAuthoritative: true;
|
|
144
|
+
sharesProcessTable: false;
|
|
145
|
+
hostLocalPaths: false;
|
|
146
|
+
externalContentTarget: false;
|
|
147
|
+
io: true;
|
|
148
|
+
keepsReadFloor: false;
|
|
144
149
|
};
|
|
145
150
|
workspaceHandle(): WorkspaceHandle;
|
|
146
151
|
/**
|
|
@@ -254,7 +259,14 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
254
259
|
abortSignal?: AbortSignal;
|
|
255
260
|
}): Promise<Result<string[], FileError>>;
|
|
256
261
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
262
|
+
/** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;本腿是原生 SDK 腿,与四条
|
|
263
|
+
* shell 腿共用的是这只编排,不是 `posix-shell-fs` 基座;父目录与私有暂存目录由编排的准备那一步建):上传 = `files.write` 整份写到私有目录里的
|
|
264
|
+
* 暂存件;提交 = 一次 `exec`。两者同一身份(`files.write` 与 `commands.run` 都不带 `user`,沙箱侧取同一个缺省用户)。 */
|
|
265
|
+
private stagedLane;
|
|
266
|
+
/** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `files.write` 直落目标:上传中途断 / 取消时目标剩什么取决于沙箱侧的落地实现,本适配器不依赖它。 */
|
|
257
267
|
writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
268
|
+
/** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份 `files.write`(O(文件);中途断 ⇒
|
|
269
|
+
* 半份)。目录不收新名字 / 私有目录收不成私有时的退路 = 旧形(`rewriteAppend`:读旧 + 拼 + 整份 `files.write` 到目标;只有写那一步经 `ctx.inPlace`)。 */
|
|
258
270
|
appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
259
271
|
fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
260
272
|
listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
@@ -340,6 +352,8 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
|
|
|
340
352
|
private tempResult;
|
|
341
353
|
private toFileInfo;
|
|
342
354
|
private fileError;
|
|
355
|
+
/** A thrown provider error → this exec's error. The two cuts (aborted, the provider's own command deadline) are a command that RAN: they carry what it had
|
|
356
|
+
* written (`captured`, `createCutError`); an unclassified fault does not (its outcome is unknown, the engine reads no partial output from it). */
|
|
343
357
|
private execError;
|
|
344
358
|
private remoteError;
|
|
345
359
|
}
|
|
@@ -2,8 +2,9 @@ import path from "node:path";
|
|
|
2
2
|
import { Sandbox, CommandExitError, FileType } from "e2b";
|
|
3
3
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
4
4
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
5
|
-
import { numEnvOr, runSymlinkChain, shellQuote } from "./remote-shell.js";
|
|
5
|
+
import { appendFileStaged, rewriteAppend, numEnvOr, runSymlinkChain, shellQuote, writeFileStaged } from "./remote-shell.js";
|
|
6
6
|
import { buildLivenessCut, commandBoundsOf, firstBoundOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
|
|
7
|
+
import { createCutError } from "../exec-cut.js";
|
|
7
8
|
import { BackgroundShellManager } from "./background-shell-support.js";
|
|
8
9
|
const PROVIDER = "e2b";
|
|
9
10
|
const DEFAULT_MOUNT_PATH = "/home/user";
|
|
@@ -46,7 +47,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
46
47
|
this.homeDir = config.homeDir;
|
|
47
48
|
this.sandboxApi = deps.sandboxApi ?? Sandbox;
|
|
48
49
|
}
|
|
49
|
-
capabilities = { isolation: true, suspendable: true, canonicalPathAuthoritative: true, sharesProcessTable: false };
|
|
50
|
+
capabilities = { isolation: true, suspendable: true, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
|
|
50
51
|
workspaceHandle() {
|
|
51
52
|
if (!this.handle) {
|
|
52
53
|
throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
|
|
@@ -251,6 +252,8 @@ export class RemoteContainerExecutionEnv {
|
|
|
251
252
|
stderr: { tail: new RollingTailBuffer(), total: 0, cursor: 0 },
|
|
252
253
|
}
|
|
253
254
|
: undefined;
|
|
255
|
+
let started;
|
|
256
|
+
const captured = () => ({ stdout: started?.stdout ?? "", stderr: started?.stderr ?? "" });
|
|
254
257
|
let detached = false;
|
|
255
258
|
const teePush = (s, d) => {
|
|
256
259
|
const b = Buffer.from(d, "utf8");
|
|
@@ -299,10 +302,10 @@ export class RemoteContainerExecutionEnv {
|
|
|
299
302
|
this.settleAbandonedStart(starting);
|
|
300
303
|
const cut = e instanceof RemoteExecutionError && e.code === "timeout" && !options?.abortSignal?.aborted;
|
|
301
304
|
if (!cut)
|
|
302
|
-
return { ok: false, error: this.execError(e, options?.abortSignal) };
|
|
303
|
-
|
|
304
|
-
return { ok: false, error: new ExecutionError(bc.code, bc.message, undefined, undefined, bc.noOutputSec) };
|
|
305
|
+
return { ok: false, error: this.execError(e, options?.abortSignal, captured()) };
|
|
306
|
+
return { ok: false, error: createCutError(boundCut(first), captured()) };
|
|
305
307
|
}
|
|
308
|
+
started = handle;
|
|
306
309
|
const onAbort = () => { if (!detached)
|
|
307
310
|
void handle.kill().catch(() => { }); };
|
|
308
311
|
if (options?.abortSignal) {
|
|
@@ -352,6 +355,7 @@ export class RemoteContainerExecutionEnv {
|
|
|
352
355
|
disarmed: () => detached,
|
|
353
356
|
...(tee ? { wall: { totalMs: this.commandTimeoutMs(options?.timeout), leftMs: Math.max(0, this.commandTimeoutMs(options?.timeout) - (Date.now() - startedAt)) } } : {}),
|
|
354
357
|
liveness: bounds.liveness,
|
|
358
|
+
captured,
|
|
355
359
|
});
|
|
356
360
|
const winner = await (detachedP ? Promise.race([fg, detachedP]) : fg);
|
|
357
361
|
if (winner === "detached") {
|
|
@@ -365,18 +369,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
365
369
|
});
|
|
366
370
|
}
|
|
367
371
|
if (options?.abortSignal?.aborted && !detached) {
|
|
368
|
-
return { ok: false, error:
|
|
372
|
+
return { ok: false, error: createCutError({ code: "aborted", message: "exec aborted (remote process killed)" }, captured()) };
|
|
369
373
|
}
|
|
370
374
|
return ok({ stdout: winner.stdout, stderr: winner.stderr, exitCode: winner.exitCode });
|
|
371
375
|
}
|
|
372
376
|
catch (e) {
|
|
373
377
|
if (options?.abortSignal?.aborted && !detached) {
|
|
374
|
-
return { ok: false, error: this.execError(e, options.abortSignal) };
|
|
378
|
+
return { ok: false, error: this.execError(e, options.abortSignal, captured()) };
|
|
375
379
|
}
|
|
376
380
|
if (e instanceof CommandExitError) {
|
|
377
381
|
return ok({ stdout: e.stdout, stderr: e.stderr, exitCode: e.exitCode });
|
|
378
382
|
}
|
|
379
|
-
return { ok: false, error: this.execError(e, options?.abortSignal) };
|
|
383
|
+
return { ok: false, error: this.execError(e, options?.abortSignal, captured()) };
|
|
380
384
|
}
|
|
381
385
|
finally {
|
|
382
386
|
fgSettled = true;
|
|
@@ -659,37 +663,33 @@ export class RemoteContainerExecutionEnv {
|
|
|
659
663
|
return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
|
|
660
664
|
}
|
|
661
665
|
}
|
|
666
|
+
stagedLane(sbx, content) {
|
|
667
|
+
const data = typeof content === "string" ? content : toArrayBuffer(content);
|
|
668
|
+
return {
|
|
669
|
+
upload: async (dest, signal) => {
|
|
670
|
+
try {
|
|
671
|
+
await this.rpc("files.write", (s) => sbx.files.write(dest, data, { signal: s }), signal, this.cfg.dataTimeoutMs);
|
|
672
|
+
return ok(undefined);
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
return { ok: false, error: this.fileError(e, dest, signal) };
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
run: (cmd, signal) => this.exec(cmd, { cwd: "/", ...(signal !== undefined ? { abortSignal: signal } : {}) }),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
662
681
|
async writeFile(p, content, abortSignal) {
|
|
663
682
|
const sbx = await this.fsReady(p);
|
|
664
683
|
if (!sbx.ok)
|
|
665
684
|
return sbx;
|
|
666
|
-
|
|
667
|
-
const data = typeof content === "string" ? content : toArrayBuffer(content);
|
|
668
|
-
await this.rpc("files.write", (signal) => sbx.value.files.write(this.resolve(p), data, { signal }), abortSignal, this.cfg.dataTimeoutMs);
|
|
669
|
-
return ok(undefined);
|
|
670
|
-
}
|
|
671
|
-
catch (e) {
|
|
672
|
-
return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
|
|
673
|
-
}
|
|
685
|
+
return writeFileStaged(this.stagedLane(sbx.value, content), this.resolve(p), abortSignal);
|
|
674
686
|
}
|
|
675
687
|
async appendFile(p, content, abortSignal) {
|
|
688
|
+
const sbx = await this.fsReady(p);
|
|
689
|
+
if (!sbx.ok)
|
|
690
|
+
return sbx;
|
|
676
691
|
const abs = this.resolve(p);
|
|
677
|
-
|
|
678
|
-
let base;
|
|
679
|
-
if (existing.ok) {
|
|
680
|
-
base = existing.value;
|
|
681
|
-
}
|
|
682
|
-
else if (existing.error.code === "not_found") {
|
|
683
|
-
base = new Uint8Array(0);
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
return existing;
|
|
687
|
-
}
|
|
688
|
-
const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
689
|
-
const merged = new Uint8Array(base.length + add.length);
|
|
690
|
-
merged.set(base, 0);
|
|
691
|
-
merged.set(add, base.length);
|
|
692
|
-
return this.writeFile(abs, merged, abortSignal);
|
|
692
|
+
return appendFileStaged(this.stagedLane(sbx.value, content), abs, abortSignal, (ctx) => rewriteAppend((signal) => this.readBinaryFile(abs, signal), (bytes, signal) => this.stagedLane(sbx.value, bytes).upload(abs, signal), content, ctx));
|
|
693
693
|
}
|
|
694
694
|
async fileInfo(p, abortSignal) {
|
|
695
695
|
const sbx = await this.fsReady(p);
|
|
@@ -851,16 +851,15 @@ export class RemoteContainerExecutionEnv {
|
|
|
851
851
|
async waitWithLiveness(handle, lastProgress, opts) {
|
|
852
852
|
let timer;
|
|
853
853
|
let wallTimer;
|
|
854
|
-
const liveness = opts
|
|
854
|
+
const liveness = opts.liveness;
|
|
855
855
|
const idle = new Promise((_, reject) => {
|
|
856
856
|
const arm = (bound) => {
|
|
857
857
|
timer = setTimeout(() => {
|
|
858
|
-
if (opts
|
|
858
|
+
if (opts.disarmed?.())
|
|
859
859
|
return;
|
|
860
860
|
const idleFor = Date.now() - lastProgress();
|
|
861
861
|
if (idleFor >= bound.ms) {
|
|
862
|
-
|
|
863
|
-
reject(new ExecutionError(cut.code, cut.message, undefined, undefined, cut.noOutputSec));
|
|
862
|
+
reject(createCutError(buildLivenessCut(bound), opts.captured()));
|
|
864
863
|
void handle.kill().catch(() => { });
|
|
865
864
|
this.cfg.metrics?.inc("remote_env_exec_idle_timeout_total", { provider: PROVIDER });
|
|
866
865
|
}
|
|
@@ -871,18 +870,18 @@ export class RemoteContainerExecutionEnv {
|
|
|
871
870
|
};
|
|
872
871
|
if (liveness !== undefined)
|
|
873
872
|
arm(liveness);
|
|
874
|
-
if (opts
|
|
873
|
+
if (opts.wall !== undefined) {
|
|
875
874
|
const wall = opts.wall;
|
|
876
875
|
wallTimer = setTimeout(() => {
|
|
877
876
|
if (opts.disarmed?.())
|
|
878
877
|
return;
|
|
879
|
-
reject(
|
|
878
|
+
reject(createCutError({ code: "timeout", message: wallCutMessage(wall.totalMs) }, opts.captured()));
|
|
880
879
|
void handle.kill().catch(() => { });
|
|
881
880
|
}, wall.leftMs);
|
|
882
881
|
}
|
|
883
882
|
});
|
|
884
883
|
try {
|
|
885
|
-
return await Promise.race([opts
|
|
884
|
+
return await Promise.race([opts.waitP ?? handle.wait(), idle]);
|
|
886
885
|
}
|
|
887
886
|
finally {
|
|
888
887
|
if (timer)
|
|
@@ -953,15 +952,15 @@ export class RemoteContainerExecutionEnv {
|
|
|
953
952
|
return new FileError("is_directory", msg, p);
|
|
954
953
|
return new FileError("unknown", msg, p);
|
|
955
954
|
}
|
|
956
|
-
execError(e, abortSignal) {
|
|
955
|
+
execError(e, abortSignal, captured) {
|
|
957
956
|
if (e instanceof ExecutionError)
|
|
958
957
|
return e;
|
|
959
958
|
const msg = errMsg(e);
|
|
960
959
|
const name = e?.name;
|
|
961
960
|
if (abortSignal?.aborted || name === "AbortError")
|
|
962
|
-
return
|
|
961
|
+
return createCutError({ code: "aborted", message: msg }, captured);
|
|
963
962
|
if (providerTimedOut(e))
|
|
964
|
-
return
|
|
963
|
+
return createCutError({ code: "timeout", message: msg }, captured);
|
|
965
964
|
return new ExecutionError("unknown", msg, e instanceof Error ? e : undefined);
|
|
966
965
|
}
|
|
967
966
|
remoteError(e, code = "unknown") {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import { FileError, type ExecutionError, type RemoteExecutionError } from "@sema-agent/core";
|
|
19
19
|
/** exec 层的错误码 → 文件层的码。只映射**有对应语义**的那些,其余落 `unknown`(诚实降级)。 */
|
|
20
20
|
export declare function fileErrorFromExec(e: ExecutionError | RemoteExecutionError, path?: string): FileError;
|
|
21
|
+
/** POSIX 的一个名字段上限(NAME_MAX,字节;Linux / macOS / 常见文件系统同值)。也是远端写的私有暂存目录名的上限(`remote-shell.ts` `buildStagingPaths`)。 */
|
|
22
|
+
export declare const NAME_MAX_BYTES = 255;
|
|
21
23
|
/**
|
|
22
24
|
* core 7.56.0 [ref]:`ENAMETOOLONG` 的**一只读法**(本文件的 stderr 分类器与 host 车道的 errno 映射同调)—— 路径有一段超过 NAME_MAX(按 UTF-8 字节量)⇒
|
|
23
25
|
* `name_too_long`:没有目录装得下这个名字,引擎读站把它读作缺席(与 `not_found` 同读);段都短、只是整条超 PATH_MAX ⇒ `unknown`(那种文件可能存在,从下层目录够得着)。
|
|
@@ -3,7 +3,7 @@ export function fileErrorFromExec(e, path) {
|
|
|
3
3
|
const code = e.code === "aborted" ? "aborted" : "unknown";
|
|
4
4
|
return new FileError(code, e.message, path, e);
|
|
5
5
|
}
|
|
6
|
-
const NAME_MAX_BYTES = 255;
|
|
6
|
+
export const NAME_MAX_BYTES = 255;
|
|
7
7
|
export function nameTooLongCode(p) {
|
|
8
8
|
return p !== undefined && p.split(/[\\/]/).some((part) => Buffer.byteLength(part, "utf8") > NAME_MAX_BYTES) ? "name_too_long" : "unknown";
|
|
9
9
|
}
|