@opengeni/runtime 0.13.0 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-65UDUFUO.js → chunk-JGUQSAG5.js} +1887 -172
- package/dist/chunk-JGUQSAG5.js.map +1 -0
- package/dist/{chunk-HAN5D27J.js → chunk-SSB7VOLE.js} +10 -1
- package/dist/chunk-SSB7VOLE.js.map +1 -0
- package/dist/{index-DGUrY_yR.d.ts → index-DU-KXVlx.d.ts} +328 -25
- package/dist/index.d.ts +25 -3
- package/dist/index.js +65 -673
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +3 -1
- package/dist/mcp-network.js +5 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +39 -1
- package/package.json +3 -3
- package/src/index.ts +20 -7
- package/src/mcp-network.ts +13 -0
- package/src/sandbox/channel-a.ts +92 -46
- package/src/sandbox/exec-banner.ts +15 -0
- package/src/sandbox/index.ts +360 -145
- package/src/sandbox/provider-errors.ts +175 -0
- package/src/sandbox/routing/backend-resolver.ts +15 -0
- package/src/sandbox/routing/routing-session.ts +730 -36
- package/src/sandbox/select.ts +8 -0
- package/src/sandbox/turn-tool-cancellation.ts +366 -92
- package/src/sandbox/workspace-archive.ts +289 -0
- package/dist/chunk-65UDUFUO.js.map +0 -1
- package/dist/chunk-HAN5D27J.js.map +0 -1
|
@@ -17492,6 +17492,295 @@ function assertProviderRegistryInvariants() {
|
|
|
17492
17492
|
}
|
|
17493
17493
|
assertProviderRegistryInvariants();
|
|
17494
17494
|
|
|
17495
|
+
// src/sandbox/provider-errors.ts
|
|
17496
|
+
var GRPC_TRANSIENT = /* @__PURE__ */ new Set([1, 2, 4, 13, 14]);
|
|
17497
|
+
var TRANSIENT_CODES = /* @__PURE__ */ new Set([
|
|
17498
|
+
"CANCELLED",
|
|
17499
|
+
"UNKNOWN",
|
|
17500
|
+
"DEADLINE_EXCEEDED",
|
|
17501
|
+
"INTERNAL",
|
|
17502
|
+
"UNAVAILABLE",
|
|
17503
|
+
"ECONNRESET",
|
|
17504
|
+
"ECONNREFUSED",
|
|
17505
|
+
"ETIMEDOUT",
|
|
17506
|
+
"EAI_AGAIN",
|
|
17507
|
+
"ENOTFOUND"
|
|
17508
|
+
]);
|
|
17509
|
+
var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["404", "NOT_FOUND", "RESOURCE_NOT_FOUND", "SANDBOX_NOT_FOUND"]);
|
|
17510
|
+
var SAFE_CODE = /^[A-Z0-9._:-]{1,64}$/;
|
|
17511
|
+
function safeRead(record, key) {
|
|
17512
|
+
try {
|
|
17513
|
+
return Reflect.get(record, key);
|
|
17514
|
+
} catch {
|
|
17515
|
+
return void 0;
|
|
17516
|
+
}
|
|
17517
|
+
}
|
|
17518
|
+
function collectSignals(value) {
|
|
17519
|
+
const out = { numbers: [], codes: [], messages: [] };
|
|
17520
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17521
|
+
const visit = (current, depth) => {
|
|
17522
|
+
if (depth > 3 || current === null || current === void 0) return;
|
|
17523
|
+
if (typeof current === "string") {
|
|
17524
|
+
if (current.length <= 512) out.messages.push(current);
|
|
17525
|
+
return;
|
|
17526
|
+
}
|
|
17527
|
+
if (typeof current !== "object" || seen.has(current)) return;
|
|
17528
|
+
seen.add(current);
|
|
17529
|
+
for (const key of [
|
|
17530
|
+
"status",
|
|
17531
|
+
"statusCode",
|
|
17532
|
+
"httpStatus",
|
|
17533
|
+
"httpStatusCode",
|
|
17534
|
+
"responseStatus",
|
|
17535
|
+
"errorCode",
|
|
17536
|
+
"code"
|
|
17537
|
+
]) {
|
|
17538
|
+
const signal = safeRead(current, key);
|
|
17539
|
+
if (typeof signal === "number" && Number.isFinite(signal)) out.numbers.push(signal);
|
|
17540
|
+
if (typeof signal === "string") {
|
|
17541
|
+
const code = signal.trim().toUpperCase();
|
|
17542
|
+
if (SAFE_CODE.test(code)) out.codes.push(code);
|
|
17543
|
+
}
|
|
17544
|
+
}
|
|
17545
|
+
for (const key of ["name", "message", "details", "cause"]) {
|
|
17546
|
+
const signal = safeRead(current, key);
|
|
17547
|
+
if (typeof signal === "string" && signal.length <= 512) out.messages.push(signal);
|
|
17548
|
+
else visit(signal, depth + 1);
|
|
17549
|
+
}
|
|
17550
|
+
visit(safeRead(current, "response"), depth + 1);
|
|
17551
|
+
visit(safeRead(current, "error"), depth + 1);
|
|
17552
|
+
};
|
|
17553
|
+
visit(value, 0);
|
|
17554
|
+
return out;
|
|
17555
|
+
}
|
|
17556
|
+
function safeDiagnostic(signals) {
|
|
17557
|
+
const code = signals.codes.find(Boolean);
|
|
17558
|
+
const numeric = signals.numbers.find((value) => Number.isFinite(value));
|
|
17559
|
+
return [code ? `code=${code}` : "", numeric !== void 0 ? `status=${numeric}` : ""].filter(Boolean).join(" ");
|
|
17560
|
+
}
|
|
17561
|
+
function hasTransientSignal(signals) {
|
|
17562
|
+
return signals.codes.some((code) => TRANSIENT_CODES.has(code)) || signals.numbers.some((status) => GRPC_TRANSIENT.has(status));
|
|
17563
|
+
}
|
|
17564
|
+
function hasModalTerminalSandboxMessage(signals) {
|
|
17565
|
+
return signals.messages.some(
|
|
17566
|
+
(message) => /^Modal sandbox [A-Za-z0-9_-]+ is no longer running\.$/.test(message) || /^Modal sandbox [A-Za-z0-9_-]+ not found \(has been terminated\)$/.test(message)
|
|
17567
|
+
);
|
|
17568
|
+
}
|
|
17569
|
+
function classifyProviderSandboxFailure(backendId, error) {
|
|
17570
|
+
if (backendId === "selfhosted" || !error) {
|
|
17571
|
+
return { kind: "other", diagnostic: "selfhosted-or-empty" };
|
|
17572
|
+
}
|
|
17573
|
+
const signals = collectSignals(error);
|
|
17574
|
+
const diagnostic = safeDiagnostic(signals) || "unclassified_provider_failure";
|
|
17575
|
+
if (hasTransientSignal(signals)) {
|
|
17576
|
+
return { kind: "transient_transport", diagnostic };
|
|
17577
|
+
}
|
|
17578
|
+
if (signals.numbers.some((status) => status === 404) || signals.codes.some((code) => NOT_FOUND_CODES.has(code))) {
|
|
17579
|
+
return { kind: "not_found", diagnostic };
|
|
17580
|
+
}
|
|
17581
|
+
if (backendId === "modal" && hasModalTerminalSandboxMessage(signals)) {
|
|
17582
|
+
return {
|
|
17583
|
+
kind: "not_found",
|
|
17584
|
+
diagnostic: diagnostic === "unclassified_provider_failure" ? "modal_terminal_message" : `${diagnostic} modal_terminal_message`
|
|
17585
|
+
};
|
|
17586
|
+
}
|
|
17587
|
+
return { kind: "other", diagnostic };
|
|
17588
|
+
}
|
|
17589
|
+
function isProviderSandboxNotFoundError2(backendId, error) {
|
|
17590
|
+
return classifyProviderSandboxFailure(backendId, error).kind === "not_found";
|
|
17591
|
+
}
|
|
17592
|
+
function isProviderSandboxGoneDuringRoutedOperation(backendId, error) {
|
|
17593
|
+
if (backendId === "selfhosted" || !error) return false;
|
|
17594
|
+
const signals = collectSignals(error);
|
|
17595
|
+
if (hasTransientSignal(signals)) return false;
|
|
17596
|
+
if (signals.codes.includes("SANDBOX_NOT_FOUND")) return true;
|
|
17597
|
+
return backendId === "modal" && hasModalTerminalSandboxMessage(signals);
|
|
17598
|
+
}
|
|
17599
|
+
function isProviderSandboxTransientError(backendId, error) {
|
|
17600
|
+
return classifyProviderSandboxFailure(backendId, error).kind === "transient_transport";
|
|
17601
|
+
}
|
|
17602
|
+
|
|
17603
|
+
// src/sandbox/workspace-archive.ts
|
|
17604
|
+
import { createHash as createHash4 } from "crypto";
|
|
17605
|
+
var WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION = 1;
|
|
17606
|
+
var WorkspaceArchiveIntegrityError = class extends Error {
|
|
17607
|
+
constructor(code, message, options = {}) {
|
|
17608
|
+
super(message);
|
|
17609
|
+
this.code = code;
|
|
17610
|
+
this.retryable = options.retryable ?? false;
|
|
17611
|
+
}
|
|
17612
|
+
name = "WorkspaceArchiveIntegrityError";
|
|
17613
|
+
retryable;
|
|
17614
|
+
};
|
|
17615
|
+
var SHA256 = /^[a-f0-9]{64}$/;
|
|
17616
|
+
var REVISION = /^wa1:[0-9]{13}:[a-f0-9]{64}$/;
|
|
17617
|
+
function sha256(bytes) {
|
|
17618
|
+
return createHash4("sha256").update(bytes).digest("hex");
|
|
17619
|
+
}
|
|
17620
|
+
function nonnegativeInteger(value) {
|
|
17621
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
17622
|
+
}
|
|
17623
|
+
function parseWorkspaceArchiveDescriptor(value) {
|
|
17624
|
+
if (!value || typeof value !== "object") return null;
|
|
17625
|
+
const candidate = value;
|
|
17626
|
+
const workspace = candidate.workspace;
|
|
17627
|
+
if (candidate.version !== WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION || typeof candidate.revision !== "string" || !REVISION.test(candidate.revision) || typeof candidate.archiveSha256 !== "string" || !SHA256.test(candidate.archiveSha256) || !nonnegativeInteger(candidate.archiveBytes) || candidate.archiveBytes === 0 || typeof candidate.capturedAt !== "string" || !Number.isFinite(Date.parse(candidate.capturedAt)) || !workspace || workspace.algorithm !== "sha256" || typeof workspace.sha256 !== "string" || !SHA256.test(workspace.sha256) || !nonnegativeInteger(workspace.entryCount) || !nonnegativeInteger(workspace.fileCount) || !nonnegativeInteger(workspace.totalFileBytes)) {
|
|
17628
|
+
return null;
|
|
17629
|
+
}
|
|
17630
|
+
return candidate;
|
|
17631
|
+
}
|
|
17632
|
+
function decodeBase64Strict(value) {
|
|
17633
|
+
if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
|
|
17634
|
+
return null;
|
|
17635
|
+
}
|
|
17636
|
+
const bytes = Uint8Array.from(Buffer.from(value, "base64"));
|
|
17637
|
+
return Buffer.from(bytes).toString("base64") === value ? bytes : null;
|
|
17638
|
+
}
|
|
17639
|
+
function readVerifiedWorkspaceArchive(base642, metadata) {
|
|
17640
|
+
if (base642 === void 0 || base642 === null || base642 === "") return null;
|
|
17641
|
+
if (typeof base642 !== "string") {
|
|
17642
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17643
|
+
"archive_base64_invalid",
|
|
17644
|
+
"selected workspace archive is not a base64 string"
|
|
17645
|
+
);
|
|
17646
|
+
}
|
|
17647
|
+
if (metadata === void 0 || metadata === null) {
|
|
17648
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17649
|
+
"archive_metadata_missing",
|
|
17650
|
+
"selected workspace archive has no durable revision/fingerprint metadata"
|
|
17651
|
+
);
|
|
17652
|
+
}
|
|
17653
|
+
const descriptor = parseWorkspaceArchiveDescriptor(metadata);
|
|
17654
|
+
if (!descriptor) {
|
|
17655
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17656
|
+
"archive_metadata_invalid",
|
|
17657
|
+
"selected workspace archive metadata is invalid"
|
|
17658
|
+
);
|
|
17659
|
+
}
|
|
17660
|
+
const bytes = decodeBase64Strict(base642);
|
|
17661
|
+
if (!bytes) {
|
|
17662
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17663
|
+
"archive_base64_invalid",
|
|
17664
|
+
`workspace archive revision ${descriptor.revision} has invalid base64 bytes`
|
|
17665
|
+
);
|
|
17666
|
+
}
|
|
17667
|
+
const actualHash = sha256(bytes);
|
|
17668
|
+
if (bytes.length !== descriptor.archiveBytes || actualHash !== descriptor.archiveSha256) {
|
|
17669
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17670
|
+
"archive_hash_mismatch",
|
|
17671
|
+
`workspace archive revision ${descriptor.revision} failed SHA-256/size verification`
|
|
17672
|
+
);
|
|
17673
|
+
}
|
|
17674
|
+
return { bytes, base64: base642, descriptor };
|
|
17675
|
+
}
|
|
17676
|
+
function stdoutFromExecResult(value) {
|
|
17677
|
+
if (typeof value === "string") return value;
|
|
17678
|
+
if (!value || typeof value !== "object") return "";
|
|
17679
|
+
const output = value;
|
|
17680
|
+
return typeof output.stdout === "string" ? output.stdout : typeof output.output === "string" ? output.output : "";
|
|
17681
|
+
}
|
|
17682
|
+
var WORKSPACE_FINGERPRINT_COMMAND = String.raw`bash -o pipefail -c '
|
|
17683
|
+
set -eu
|
|
17684
|
+
cd /workspace
|
|
17685
|
+
digest=$(LC_ALL=C tar --sort=name --mtime="@0" --owner=0 --group=0 --numeric-owner --format=gnu -cf - . | sha256sum | awk "{print \$1}")
|
|
17686
|
+
entries=$(find . -xdev -mindepth 1 -printf x | wc -c | tr -d " ")
|
|
17687
|
+
files=$(find . -xdev -type f -printf x | wc -c | tr -d " ")
|
|
17688
|
+
bytes=$(find . -xdev -type f -printf "%s\n" | awk "{s+=\$1} END {printf \"%.0f\", s+0}")
|
|
17689
|
+
printf "OPENGENI_WORKSPACE_FINGERPRINT_V1 %s %s %s %s\n" "$digest" "$entries" "$files" "$bytes"
|
|
17690
|
+
'`;
|
|
17691
|
+
async function fingerprintSandboxWorkspace(session) {
|
|
17692
|
+
const target = session;
|
|
17693
|
+
const run2 = target.exec ?? target.execCommand;
|
|
17694
|
+
if (!run2) {
|
|
17695
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17696
|
+
"workspace_fingerprint_unavailable",
|
|
17697
|
+
"sandbox session cannot execute the workspace fingerprint probe"
|
|
17698
|
+
);
|
|
17699
|
+
}
|
|
17700
|
+
const result = await run2.call(target, {
|
|
17701
|
+
cmd: WORKSPACE_FINGERPRINT_COMMAND,
|
|
17702
|
+
yieldTimeMs: 12e4,
|
|
17703
|
+
maxOutputTokens: 1e3
|
|
17704
|
+
});
|
|
17705
|
+
const match = stdoutFromExecResult(result).trim().match(/^OPENGENI_WORKSPACE_FINGERPRINT_V1 ([a-f0-9]{64}) ([0-9]+) ([0-9]+) ([0-9]+)$/);
|
|
17706
|
+
if (!match) {
|
|
17707
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17708
|
+
"workspace_fingerprint_unavailable",
|
|
17709
|
+
"sandbox workspace fingerprint probe returned no valid digest",
|
|
17710
|
+
{ retryable: true }
|
|
17711
|
+
);
|
|
17712
|
+
}
|
|
17713
|
+
const entryCount = Number(match[2]);
|
|
17714
|
+
const fileCount = Number(match[3]);
|
|
17715
|
+
const totalFileBytes = Number(match[4]);
|
|
17716
|
+
if (![entryCount, fileCount, totalFileBytes].every(Number.isSafeInteger)) {
|
|
17717
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17718
|
+
"workspace_fingerprint_unavailable",
|
|
17719
|
+
"sandbox workspace fingerprint counters exceeded the supported range"
|
|
17720
|
+
);
|
|
17721
|
+
}
|
|
17722
|
+
return {
|
|
17723
|
+
algorithm: "sha256",
|
|
17724
|
+
sha256: match[1],
|
|
17725
|
+
entryCount,
|
|
17726
|
+
fileCount,
|
|
17727
|
+
totalFileBytes
|
|
17728
|
+
};
|
|
17729
|
+
}
|
|
17730
|
+
function fingerprintsEqual(a, b) {
|
|
17731
|
+
return a.sha256 === b.sha256 && a.entryCount === b.entryCount && a.fileCount === b.fileCount && a.totalFileBytes === b.totalFileBytes;
|
|
17732
|
+
}
|
|
17733
|
+
async function captureVerifiedWorkspaceArchive(session, capturedAtMs = Date.now()) {
|
|
17734
|
+
const target = session;
|
|
17735
|
+
if (typeof target.persistWorkspace !== "function") {
|
|
17736
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17737
|
+
"workspace_fingerprint_unavailable",
|
|
17738
|
+
"sandbox session does not support workspace persistence"
|
|
17739
|
+
);
|
|
17740
|
+
}
|
|
17741
|
+
const before = await fingerprintSandboxWorkspace(target);
|
|
17742
|
+
const bytes = await target.persistWorkspace();
|
|
17743
|
+
if (!bytes || bytes.length === 0) {
|
|
17744
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17745
|
+
"archive_base64_invalid",
|
|
17746
|
+
"sandbox workspace persistence returned an empty archive",
|
|
17747
|
+
{ retryable: true }
|
|
17748
|
+
);
|
|
17749
|
+
}
|
|
17750
|
+
const after = await fingerprintSandboxWorkspace(target);
|
|
17751
|
+
if (!fingerprintsEqual(before, after)) {
|
|
17752
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17753
|
+
"workspace_changed_during_capture",
|
|
17754
|
+
"workspace changed while its durable archive was being captured",
|
|
17755
|
+
{ retryable: true }
|
|
17756
|
+
);
|
|
17757
|
+
}
|
|
17758
|
+
const archiveSha256 = sha256(bytes);
|
|
17759
|
+
const descriptor = {
|
|
17760
|
+
version: WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
|
|
17761
|
+
revision: `wa1:${String(capturedAtMs).padStart(13, "0")}:${archiveSha256}`,
|
|
17762
|
+
archiveSha256,
|
|
17763
|
+
archiveBytes: bytes.length,
|
|
17764
|
+
capturedAt: new Date(capturedAtMs).toISOString(),
|
|
17765
|
+
workspace: before
|
|
17766
|
+
};
|
|
17767
|
+
return {
|
|
17768
|
+
bytes,
|
|
17769
|
+
base64: Buffer.from(bytes).toString("base64"),
|
|
17770
|
+
descriptor
|
|
17771
|
+
};
|
|
17772
|
+
}
|
|
17773
|
+
async function verifyRestoredWorkspace(session, descriptor) {
|
|
17774
|
+
const actual = await fingerprintSandboxWorkspace(session);
|
|
17775
|
+
if (!fingerprintsEqual(actual, descriptor.workspace)) {
|
|
17776
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
17777
|
+
"workspace_fingerprint_mismatch",
|
|
17778
|
+
`restored workspace does not match selected archive revision ${descriptor.revision}`
|
|
17779
|
+
);
|
|
17780
|
+
}
|
|
17781
|
+
return actual;
|
|
17782
|
+
}
|
|
17783
|
+
|
|
17495
17784
|
// src/sandbox/index.ts
|
|
17496
17785
|
import { collectSandboxEnvironment as collectSandboxEnvironment2, parseExposedPorts as parseExposedPorts2 } from "@opengeni/config";
|
|
17497
17786
|
|
|
@@ -17708,6 +17997,9 @@ function negotiateCapabilities(ctx) {
|
|
|
17708
17997
|
os: ctx.os,
|
|
17709
17998
|
liveness: ctx.liveness,
|
|
17710
17999
|
leaseEpoch: ctx.leaseEpoch,
|
|
18000
|
+
workspaceGeneration: ctx.workspaceGeneration ?? null,
|
|
18001
|
+
archiveGeneration: ctx.archiveGeneration ?? null,
|
|
18002
|
+
archiveComplete: ctx.archiveComplete ?? false,
|
|
17711
18003
|
viewerHeartbeatIntervalMs: 3e4,
|
|
17712
18004
|
FileSystem: fileSystem,
|
|
17713
18005
|
Terminal: terminal,
|
|
@@ -17796,6 +18088,12 @@ function parseExecBannerExitCode(raw) {
|
|
|
17796
18088
|
const banner = parseExecResponseBanner(raw);
|
|
17797
18089
|
return banner.kind === "exited" ? banner.exitCode : null;
|
|
17798
18090
|
}
|
|
18091
|
+
function isExecSessionLostBanner(out, execSessionId) {
|
|
18092
|
+
if (!out || !Number.isSafeInteger(execSessionId) || execSessionId < 0) return false;
|
|
18093
|
+
const match = out.match(/^(?:write_stdin failed: )?session not found: (\d+)$/);
|
|
18094
|
+
if (!match || match[0] !== out) return false;
|
|
18095
|
+
return match[1] === String(execSessionId);
|
|
18096
|
+
}
|
|
17799
18097
|
|
|
17800
18098
|
// src/sandbox/display-stack.ts
|
|
17801
18099
|
var STREAM_PORT = DESKTOP_STREAM_PORT4;
|
|
@@ -19152,7 +19450,7 @@ function withRunCredentialsClient(client, sessionId, onSessionReady) {
|
|
|
19152
19450
|
}
|
|
19153
19451
|
|
|
19154
19452
|
// src/sandbox/toolspace-token.ts
|
|
19155
|
-
import { createHash as
|
|
19453
|
+
import { createHash as createHash5 } from "crypto";
|
|
19156
19454
|
import { posix as posixPath } from "path";
|
|
19157
19455
|
var MAX_TOOLSPACE_SESSION_ID_BYTES = 512;
|
|
19158
19456
|
var ToolspaceTokenPathError = class extends Error {
|
|
@@ -19175,7 +19473,7 @@ function toolspaceTokenFileForSession(manifestTokenFile, sessionId) {
|
|
|
19175
19473
|
throw new ToolspaceTokenPathError("Toolspace manifest token file must be absolute");
|
|
19176
19474
|
}
|
|
19177
19475
|
const normalizedManifestFile = posixPath.normalize(manifestTokenFile);
|
|
19178
|
-
const digest =
|
|
19476
|
+
const digest = createHash5("sha256").update(assertToolspaceSessionId(sessionId)).digest("hex");
|
|
19179
19477
|
return posixPath.join(posixPath.dirname(normalizedManifestFile), "toolspace-tokens", digest);
|
|
19180
19478
|
}
|
|
19181
19479
|
function toolspaceTokenFileFromEnvironment(environment, sessionId) {
|
|
@@ -19442,17 +19740,815 @@ async function uploadRecordingArtifact(session, proc, input) {
|
|
|
19442
19740
|
if (!output.includes(RECORDING_UPLOAD_OK)) {
|
|
19443
19741
|
throw new Error("recording upload did not confirm success");
|
|
19444
19742
|
}
|
|
19445
|
-
return metadata;
|
|
19446
|
-
}
|
|
19447
|
-
async function deleteRecordingArtifacts(session, proc) {
|
|
19448
|
-
const s = session;
|
|
19449
|
-
const logFile = proc.boxPath.replace(/\.(mp4|webm)$/, ".log");
|
|
19450
|
-
await run(s, `rm -f ${proc.boxPath} ${proc.pidFile} ${logFile}`, proc.runAs).catch(
|
|
19451
|
-
() => void 0
|
|
19452
|
-
);
|
|
19743
|
+
return metadata;
|
|
19744
|
+
}
|
|
19745
|
+
async function deleteRecordingArtifacts(session, proc) {
|
|
19746
|
+
const s = session;
|
|
19747
|
+
const logFile = proc.boxPath.replace(/\.(mp4|webm)$/, ".log");
|
|
19748
|
+
await run(s, `rm -f ${proc.boxPath} ${proc.pidFile} ${logFile}`, proc.runAs).catch(
|
|
19749
|
+
() => void 0
|
|
19750
|
+
);
|
|
19751
|
+
}
|
|
19752
|
+
function recordingStorageKey(workspaceId, sessionId, recordingId, codec) {
|
|
19753
|
+
return `recordings/${workspaceId}/${sessionId}/${recordingId}.${extForCodec(codec)}`;
|
|
19754
|
+
}
|
|
19755
|
+
|
|
19756
|
+
// src/sandbox/turn-tool-cancellation.ts
|
|
19757
|
+
var TURN_EXEC_YIELD_MS = 250;
|
|
19758
|
+
var TURN_WRITE_YIELD_MS = 250;
|
|
19759
|
+
var SHELL_HELPER_YIELD_MS = 1e3;
|
|
19760
|
+
var SHELL_GRACEFUL_POLLS = 2;
|
|
19761
|
+
var SHELL_POLL_MS = 100;
|
|
19762
|
+
var SHELL_MARKER_DIR = "/tmp/opengeni-turn-shell";
|
|
19763
|
+
var delay = async (milliseconds) => await new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
19764
|
+
var TurnSandboxCommandCancelledError = class extends Error {
|
|
19765
|
+
name = "TurnSandboxCommandCancelledError";
|
|
19766
|
+
constructor(reason) {
|
|
19767
|
+
super(
|
|
19768
|
+
reason instanceof Error ? reason.message : String(reason ?? "Turn sandbox tools were cancelled"),
|
|
19769
|
+
{
|
|
19770
|
+
...reason instanceof Error ? { cause: reason } : {}
|
|
19771
|
+
}
|
|
19772
|
+
);
|
|
19773
|
+
}
|
|
19774
|
+
};
|
|
19775
|
+
function cancellationError(reason) {
|
|
19776
|
+
return new TurnSandboxCommandCancelledError(reason);
|
|
19777
|
+
}
|
|
19778
|
+
function parsedObject(input) {
|
|
19779
|
+
try {
|
|
19780
|
+
const parsed = JSON.parse(input);
|
|
19781
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
19782
|
+
} catch {
|
|
19783
|
+
return null;
|
|
19784
|
+
}
|
|
19785
|
+
}
|
|
19786
|
+
function cappedYield(value, cap) {
|
|
19787
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? Math.min(value, cap) : cap;
|
|
19788
|
+
}
|
|
19789
|
+
function execOutput(raw) {
|
|
19790
|
+
const marker = "\nOutput:\n";
|
|
19791
|
+
const index = raw.indexOf(marker);
|
|
19792
|
+
return index >= 0 ? raw.slice(index + marker.length) : "";
|
|
19793
|
+
}
|
|
19794
|
+
function nativeCommandBanner(result) {
|
|
19795
|
+
if (typeof result === "string") return result;
|
|
19796
|
+
if (!result || typeof result !== "object") {
|
|
19797
|
+
throw new Error("Sandbox command returned an invalid result");
|
|
19798
|
+
}
|
|
19799
|
+
const value = result;
|
|
19800
|
+
const sessionId = typeof value.sessionId === "number" ? value.sessionId : typeof value.session_id === "number" ? value.session_id : null;
|
|
19801
|
+
const exitCode = typeof value.exitCode === "number" ? value.exitCode : typeof value.exit_code === "number" ? value.exit_code : null;
|
|
19802
|
+
const output = [value.output, value.stderr, value.stdout].filter(
|
|
19803
|
+
(candidate) => typeof candidate === "string" && candidate.length > 0
|
|
19804
|
+
).join("\n");
|
|
19805
|
+
if (sessionId !== null) {
|
|
19806
|
+
return `Process running with session ID ${sessionId}
|
|
19807
|
+
|
|
19808
|
+
Output:
|
|
19809
|
+
${output}`;
|
|
19810
|
+
}
|
|
19811
|
+
if (exitCode !== null) {
|
|
19812
|
+
return `Process exited with code ${exitCode}
|
|
19813
|
+
|
|
19814
|
+
Output:
|
|
19815
|
+
${output}`;
|
|
19816
|
+
}
|
|
19817
|
+
throw new Error("Sandbox command did not report a session id or exit code");
|
|
19818
|
+
}
|
|
19819
|
+
function nativeCommandResult(result) {
|
|
19820
|
+
if (typeof result === "string") {
|
|
19821
|
+
return {
|
|
19822
|
+
stdout: execOutput(result),
|
|
19823
|
+
stderr: "",
|
|
19824
|
+
exitCode: parseExecBannerExitCode(result),
|
|
19825
|
+
sessionId: parseExecBannerSessionId(result),
|
|
19826
|
+
wallTimeSeconds: 0
|
|
19827
|
+
};
|
|
19828
|
+
}
|
|
19829
|
+
if (!result || typeof result !== "object") {
|
|
19830
|
+
throw new Error("Sandbox command returned an invalid result");
|
|
19831
|
+
}
|
|
19832
|
+
const value = result;
|
|
19833
|
+
return {
|
|
19834
|
+
stdout: typeof value.stdout === "string" ? value.stdout : typeof value.output === "string" ? value.output : "",
|
|
19835
|
+
stderr: typeof value.stderr === "string" ? value.stderr : "",
|
|
19836
|
+
exitCode: typeof value.exitCode === "number" ? value.exitCode : typeof value.exit_code === "number" ? value.exit_code : null,
|
|
19837
|
+
sessionId: typeof value.sessionId === "number" ? value.sessionId : typeof value.session_id === "number" ? value.session_id : null,
|
|
19838
|
+
wallTimeSeconds: typeof value.wallTimeSeconds === "number" && value.wallTimeSeconds >= 0 ? value.wallTimeSeconds : 0
|
|
19839
|
+
};
|
|
19840
|
+
}
|
|
19841
|
+
function completedCommandBanner(exitCode, output) {
|
|
19842
|
+
return `Process exited with code ${exitCode}
|
|
19843
|
+
|
|
19844
|
+
Output:
|
|
19845
|
+
${output}`;
|
|
19846
|
+
}
|
|
19847
|
+
function appendBoundedOutput3(current, chunk, maxOutputTokens) {
|
|
19848
|
+
const maxChars = Math.max(512, maxOutputTokens * 4);
|
|
19849
|
+
const combined = current + chunk;
|
|
19850
|
+
return combined.length <= maxChars ? combined : combined.slice(-maxChars);
|
|
19851
|
+
}
|
|
19852
|
+
function singleQuote(value) {
|
|
19853
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
19854
|
+
}
|
|
19855
|
+
function shellMarkerPath(token) {
|
|
19856
|
+
return `${SHELL_MARKER_DIR}/${token}`;
|
|
19857
|
+
}
|
|
19858
|
+
function cancellableGroupLeaderCommand(command, markerPath) {
|
|
19859
|
+
const marker = singleQuote(markerPath);
|
|
19860
|
+
const markerDir = singleQuote(SHELL_MARKER_DIR);
|
|
19861
|
+
return [
|
|
19862
|
+
`__opengeni_marker=${marker}`,
|
|
19863
|
+
"umask 077",
|
|
19864
|
+
`command mkdir -p ${markerDir} || exit 125`,
|
|
19865
|
+
'__opengeni_pid="$$"',
|
|
19866
|
+
`__opengeni_pgid="$(command ps -o pgid= -p "$__opengeni_pid" 2>/dev/null | command tr -d '[:space:]')"`,
|
|
19867
|
+
'case "$__opengeni_pid:$__opengeni_pgid" in *[!0-9:]*|*:|:*) exit 125 ;; esac',
|
|
19868
|
+
'[ "$__opengeni_pid" -gt 1 ] && [ "$__opengeni_pgid" -gt 1 ] || exit 125',
|
|
19869
|
+
// Never signal a provider/container-wide process group. A cancellable PTY
|
|
19870
|
+
// command must be its group's leader; fail the command closed otherwise.
|
|
19871
|
+
'[ "$__opengeni_pid" = "$__opengeni_pgid" ] || exit 125',
|
|
19872
|
+
`command printf '%s %s\\n' "$__opengeni_pid" "$__opengeni_pgid" > "$__opengeni_marker" || exit 125`,
|
|
19873
|
+
`trap 'command rm -f "$__opengeni_marker"' EXIT`,
|
|
19874
|
+
"(",
|
|
19875
|
+
command,
|
|
19876
|
+
")",
|
|
19877
|
+
"__opengeni_status=$?",
|
|
19878
|
+
'exit "$__opengeni_status"'
|
|
19879
|
+
].join("\n");
|
|
19880
|
+
}
|
|
19881
|
+
function cancellableShellCommand(command, markerPath) {
|
|
19882
|
+
const groupLeaderCommand = cancellableGroupLeaderCommand(command, markerPath);
|
|
19883
|
+
return [
|
|
19884
|
+
'__opengeni_outer_pid="$$"',
|
|
19885
|
+
`__opengeni_outer_pgid="$(command ps -o pgid= -p "$__opengeni_outer_pid" 2>/dev/null | command tr -d '[:space:]')"`,
|
|
19886
|
+
'case "$__opengeni_outer_pid:$__opengeni_outer_pgid" in *[!0-9:]*|*:|:*) exit 125 ;; esac',
|
|
19887
|
+
'if [ "$__opengeni_outer_pid" != "$__opengeni_outer_pgid" ]; then',
|
|
19888
|
+
' __opengeni_setsid="$(command -v setsid 2>/dev/null)"',
|
|
19889
|
+
' [ -n "$__opengeni_setsid" ] || exit 125',
|
|
19890
|
+
` exec "$__opengeni_setsid" /bin/sh -c ${singleQuote(groupLeaderCommand)}`,
|
|
19891
|
+
"fi",
|
|
19892
|
+
groupLeaderCommand
|
|
19893
|
+
].join("\n");
|
|
19894
|
+
}
|
|
19895
|
+
function shellHelperInput(command) {
|
|
19896
|
+
const args = shellHelperArgs(command);
|
|
19897
|
+
return JSON.stringify({
|
|
19898
|
+
cmd: args.cmd,
|
|
19899
|
+
login: args.login,
|
|
19900
|
+
tty: args.tty,
|
|
19901
|
+
yield_time_ms: args.yieldTimeMs,
|
|
19902
|
+
max_output_tokens: args.maxOutputTokens
|
|
19903
|
+
});
|
|
19904
|
+
}
|
|
19905
|
+
function shellHelperArgs(command) {
|
|
19906
|
+
return {
|
|
19907
|
+
cmd: command,
|
|
19908
|
+
login: false,
|
|
19909
|
+
tty: false,
|
|
19910
|
+
yieldTimeMs: SHELL_HELPER_YIELD_MS,
|
|
19911
|
+
maxOutputTokens: 128
|
|
19912
|
+
};
|
|
19913
|
+
}
|
|
19914
|
+
function retainedProcessSession(session, providerSessionId) {
|
|
19915
|
+
return session?.hasRetainedProcess?.(providerSessionId) === true ? session : null;
|
|
19916
|
+
}
|
|
19917
|
+
function safeProcessIdentity(raw) {
|
|
19918
|
+
if (parseExecBannerExitCode(raw) !== 0) return null;
|
|
19919
|
+
const match = execOutput(raw).trim().match(/^(\d+)\s+(\d+)$/);
|
|
19920
|
+
if (!match) return null;
|
|
19921
|
+
const pid = Number.parseInt(match[1], 10);
|
|
19922
|
+
const processGroupId = Number.parseInt(match[2], 10);
|
|
19923
|
+
return Number.isSafeInteger(pid) && Number.isSafeInteger(processGroupId) && pid > 1 && processGroupId > 1 ? { pid, processGroupId } : null;
|
|
19924
|
+
}
|
|
19925
|
+
function identityGuardScript(state, body, missingIdentityExitCode) {
|
|
19926
|
+
const identity = state.identity;
|
|
19927
|
+
if (!identity || !state.token) return `exit ${missingIdentityExitCode}`;
|
|
19928
|
+
const token = singleQuote(state.token);
|
|
19929
|
+
return [
|
|
19930
|
+
`__opengeni_pid=${identity.pid}`,
|
|
19931
|
+
`__opengeni_pgid=${identity.processGroupId}`,
|
|
19932
|
+
`__opengeni_token=${token}`,
|
|
19933
|
+
// The randomized token lands late in the wrapped command line. Procps
|
|
19934
|
+
// otherwise truncates `args` to the terminal width, which can make a live
|
|
19935
|
+
// group look stale and open the quiescence fence without signalling it.
|
|
19936
|
+
'__opengeni_args="$(command ps -ww -o args= -p "$__opengeni_pid" 2>/dev/null)"',
|
|
19937
|
+
'case "$__opengeni_args" in *"$__opengeni_token"*) ;; *) exit 0 ;; esac',
|
|
19938
|
+
`__opengeni_live_pgid="$(command ps -o pgid= -p "$__opengeni_pid" 2>/dev/null | command tr -d '[:space:]')"`,
|
|
19939
|
+
'[ "$__opengeni_live_pgid" = "$__opengeni_pgid" ] || exit 0',
|
|
19940
|
+
body
|
|
19941
|
+
].join("\n");
|
|
19942
|
+
}
|
|
19943
|
+
function wrapComputer(computer, track) {
|
|
19944
|
+
const wrapped = Object.create(Object.getPrototypeOf(computer));
|
|
19945
|
+
const wrappedRecord = wrapped;
|
|
19946
|
+
Object.assign(wrapped, computer);
|
|
19947
|
+
for (const method of [
|
|
19948
|
+
"initRun",
|
|
19949
|
+
"screenshot",
|
|
19950
|
+
"click",
|
|
19951
|
+
"doubleClick",
|
|
19952
|
+
"scroll",
|
|
19953
|
+
"type",
|
|
19954
|
+
"wait",
|
|
19955
|
+
"move",
|
|
19956
|
+
"keypress",
|
|
19957
|
+
"drag"
|
|
19958
|
+
]) {
|
|
19959
|
+
const original = computer[method];
|
|
19960
|
+
if (typeof original !== "function") continue;
|
|
19961
|
+
wrappedRecord[method] = (...args) => track(async () => await original.apply(computer, args));
|
|
19962
|
+
}
|
|
19963
|
+
return wrapped;
|
|
19964
|
+
}
|
|
19965
|
+
var TurnToolCancellationControllerImpl = class {
|
|
19966
|
+
cancelled = false;
|
|
19967
|
+
reason;
|
|
19968
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
19969
|
+
shellSessions = /* @__PURE__ */ new Map();
|
|
19970
|
+
remoteExecs = /* @__PURE__ */ new Set();
|
|
19971
|
+
rawExecInvoke = null;
|
|
19972
|
+
rawWriteInvoke = null;
|
|
19973
|
+
drainPromise = null;
|
|
19974
|
+
signal;
|
|
19975
|
+
onAbort = () => this.cancel(this.signal?.reason);
|
|
19976
|
+
constructor(signal) {
|
|
19977
|
+
this.signal = signal;
|
|
19978
|
+
if (signal?.aborted) {
|
|
19979
|
+
this.cancel(signal.reason);
|
|
19980
|
+
} else {
|
|
19981
|
+
signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
19982
|
+
}
|
|
19983
|
+
}
|
|
19984
|
+
cancel(reason) {
|
|
19985
|
+
if (!this.cancelled) {
|
|
19986
|
+
this.cancelled = true;
|
|
19987
|
+
this.reason = reason;
|
|
19988
|
+
this.signal?.removeEventListener("abort", this.onAbort);
|
|
19989
|
+
}
|
|
19990
|
+
void this.ensureDrain().catch(() => void 0);
|
|
19991
|
+
}
|
|
19992
|
+
async waitForQuiescence() {
|
|
19993
|
+
this.cancel(this.reason);
|
|
19994
|
+
await this.ensureDrain();
|
|
19995
|
+
}
|
|
19996
|
+
runSandboxCommand(session, args) {
|
|
19997
|
+
return this.runSandboxCommandInternal(session, args, false);
|
|
19998
|
+
}
|
|
19999
|
+
async runSandboxCommandStructured(session, args) {
|
|
20000
|
+
return await this.runSandboxCommandInternal(session, args, true);
|
|
20001
|
+
}
|
|
20002
|
+
runSandboxCommandInternal(session, args, structured) {
|
|
20003
|
+
return this.track(async () => {
|
|
20004
|
+
const startedAt = performance.now();
|
|
20005
|
+
if (!session.exec && !session.execCommand) {
|
|
20006
|
+
throw new Error("Sandbox session does not support command execution");
|
|
20007
|
+
}
|
|
20008
|
+
const lifecycleRunContext = {};
|
|
20009
|
+
const invokeExecNative = async (input) => {
|
|
20010
|
+
const parsed = parsedObject(input);
|
|
20011
|
+
if (!parsed || typeof parsed.cmd !== "string") {
|
|
20012
|
+
throw new Error("Sandbox lifecycle command input was invalid");
|
|
20013
|
+
}
|
|
20014
|
+
const commandArgs = {
|
|
20015
|
+
cmd: parsed.cmd,
|
|
20016
|
+
...typeof parsed.workdir === "string" ? { workdir: parsed.workdir } : {},
|
|
20017
|
+
...typeof parsed.shell === "string" ? { shell: parsed.shell } : {},
|
|
20018
|
+
...typeof parsed.login === "boolean" ? { login: parsed.login } : {},
|
|
20019
|
+
...typeof parsed.tty === "boolean" ? { tty: parsed.tty } : {},
|
|
20020
|
+
...typeof parsed.yield_time_ms === "number" ? { yieldTimeMs: parsed.yield_time_ms } : {},
|
|
20021
|
+
...typeof parsed.max_output_tokens === "number" ? { maxOutputTokens: parsed.max_output_tokens } : {},
|
|
20022
|
+
...typeof parsed.run_as === "string" ? { runAs: parsed.run_as } : {}
|
|
20023
|
+
};
|
|
20024
|
+
const result = session.supportsPty?.() !== false && session.execCommand ? await session.execCommand(commandArgs) : session.exec ? await session.exec(commandArgs) : await session.execCommand(commandArgs);
|
|
20025
|
+
return result;
|
|
20026
|
+
};
|
|
20027
|
+
const invokeExec = async (_runContext, input) => nativeCommandBanner(await invokeExecNative(input));
|
|
20028
|
+
const invokeWrite = session.writeStdin || session.writeStdinForProcessControl ? async (_runContext, input) => {
|
|
20029
|
+
const parsed = parsedObject(input);
|
|
20030
|
+
if (!parsed || typeof parsed.session_id !== "number") {
|
|
20031
|
+
throw new Error("Sandbox lifecycle stdin input was invalid");
|
|
20032
|
+
}
|
|
20033
|
+
const directArgs = {
|
|
20034
|
+
sessionId: parsed.session_id,
|
|
20035
|
+
...typeof parsed.chars === "string" ? { chars: parsed.chars } : {},
|
|
20036
|
+
...typeof parsed.yield_time_ms === "number" ? { yieldTimeMs: parsed.yield_time_ms } : {},
|
|
20037
|
+
...typeof parsed.max_output_tokens === "number" ? { maxOutputTokens: parsed.max_output_tokens } : {}
|
|
20038
|
+
};
|
|
20039
|
+
if (session.hasRetainedProcess?.(parsed.session_id) === true && session.writeStdinForProcessControl) {
|
|
20040
|
+
return await session.writeStdinForProcessControl(directArgs);
|
|
20041
|
+
}
|
|
20042
|
+
if (!session.writeStdin) {
|
|
20043
|
+
throw new Error(
|
|
20044
|
+
"Sandbox lifecycle command yielded an unretained session without stdin support"
|
|
20045
|
+
);
|
|
20046
|
+
}
|
|
20047
|
+
return await session.writeStdin(directArgs);
|
|
20048
|
+
} : null;
|
|
20049
|
+
const correlationId = `turn_lifecycle_${crypto.randomUUID()}`;
|
|
20050
|
+
const useRemoteOpCancellation = Boolean(session.cancelExecCommand) && session.supportsPty?.() === false;
|
|
20051
|
+
const remoteExec = session.cancelExecCommand && useRemoteOpCancellation ? this.registerRemoteExec(
|
|
20052
|
+
{ cancelExecCommand: session.cancelExecCommand.bind(session) },
|
|
20053
|
+
`${sanitizeOpIdToken(correlationId)}:0`
|
|
20054
|
+
) : null;
|
|
20055
|
+
const markerPath = shellMarkerPath(crypto.randomUUID());
|
|
20056
|
+
const commandInput = JSON.stringify({
|
|
20057
|
+
cmd: useRemoteOpCancellation ? args.cmd : cancellableShellCommand(args.cmd, markerPath),
|
|
20058
|
+
...args.workdir ? { workdir: args.workdir } : {},
|
|
20059
|
+
...args.shell ? { shell: args.shell } : {},
|
|
20060
|
+
...args.login !== void 0 ? { login: args.login } : {},
|
|
20061
|
+
...args.runAs ? { run_as: args.runAs } : {},
|
|
20062
|
+
tty: useRemoteOpCancellation ? args.tty : true,
|
|
20063
|
+
yield_time_ms: useRemoteOpCancellation ? args.yieldTimeMs : cappedYield(args.yieldTimeMs, TURN_EXEC_YIELD_MS),
|
|
20064
|
+
...args.maxOutputTokens !== void 0 ? { max_output_tokens: args.maxOutputTokens } : {}
|
|
20065
|
+
});
|
|
20066
|
+
let initialNative;
|
|
20067
|
+
try {
|
|
20068
|
+
initialNative = await runWithToolCallCorrelation(
|
|
20069
|
+
correlationId,
|
|
20070
|
+
async () => await invokeExecNative(commandInput)
|
|
20071
|
+
);
|
|
20072
|
+
} finally {
|
|
20073
|
+
remoteExec?.settle();
|
|
20074
|
+
}
|
|
20075
|
+
const initial = nativeCommandBanner(initialNative);
|
|
20076
|
+
const initialResult = nativeCommandResult(initialNative);
|
|
20077
|
+
if (useRemoteOpCancellation) {
|
|
20078
|
+
if (!structured) return initial;
|
|
20079
|
+
if (initialResult.exitCode === null) {
|
|
20080
|
+
throw new Error("Sandbox command did not report a terminal exit code");
|
|
20081
|
+
}
|
|
20082
|
+
return {
|
|
20083
|
+
stdout: initialResult.stdout,
|
|
20084
|
+
stderr: initialResult.stderr,
|
|
20085
|
+
exitCode: initialResult.exitCode,
|
|
20086
|
+
wallTimeSeconds: Math.max(
|
|
20087
|
+
initialResult.wallTimeSeconds,
|
|
20088
|
+
(performance.now() - startedAt) / 1e3
|
|
20089
|
+
)
|
|
20090
|
+
};
|
|
20091
|
+
}
|
|
20092
|
+
const sessionId = parseExecBannerSessionId(initial);
|
|
20093
|
+
if (sessionId === null) {
|
|
20094
|
+
if (!structured) return initial;
|
|
20095
|
+
if (initialResult.exitCode === null) {
|
|
20096
|
+
throw new Error("Sandbox command did not report a terminal exit code");
|
|
20097
|
+
}
|
|
20098
|
+
return {
|
|
20099
|
+
stdout: initialResult.stdout,
|
|
20100
|
+
stderr: initialResult.stderr,
|
|
20101
|
+
exitCode: initialResult.exitCode,
|
|
20102
|
+
wallTimeSeconds: Math.max(
|
|
20103
|
+
initialResult.wallTimeSeconds,
|
|
20104
|
+
(performance.now() - startedAt) / 1e3
|
|
20105
|
+
)
|
|
20106
|
+
};
|
|
20107
|
+
}
|
|
20108
|
+
if (!invokeWrite) {
|
|
20109
|
+
throw new Error("Sandbox lifecycle command yielded without stdin support");
|
|
20110
|
+
}
|
|
20111
|
+
const token = markerPath.slice(markerPath.lastIndexOf("/") + 1);
|
|
20112
|
+
const state = {
|
|
20113
|
+
sessionId,
|
|
20114
|
+
markerPath,
|
|
20115
|
+
token,
|
|
20116
|
+
runContext: lifecycleRunContext,
|
|
20117
|
+
execInvoke: invokeExec,
|
|
20118
|
+
writeInvoke: invokeWrite,
|
|
20119
|
+
processSession: retainedProcessSession(session, sessionId),
|
|
20120
|
+
identity: null,
|
|
20121
|
+
identityValidated: false,
|
|
20122
|
+
cancellation: null
|
|
20123
|
+
};
|
|
20124
|
+
this.shellSessions.set(sessionId, state);
|
|
20125
|
+
const maxOutputTokens = args.maxOutputTokens ?? 2e4;
|
|
20126
|
+
const initialOutput = execOutput(initial);
|
|
20127
|
+
let output = initialOutput ? appendBoundedOutput3("", initialOutput, maxOutputTokens) : "";
|
|
20128
|
+
let stdout = appendBoundedOutput3("", initialResult.stdout, maxOutputTokens);
|
|
20129
|
+
const stderr = appendBoundedOutput3("", initialResult.stderr, maxOutputTokens);
|
|
20130
|
+
while (true) {
|
|
20131
|
+
if (this.cancelled) throw cancellationError(this.reason);
|
|
20132
|
+
const next = await invokeWrite(
|
|
20133
|
+
lifecycleRunContext,
|
|
20134
|
+
JSON.stringify({
|
|
20135
|
+
session_id: sessionId,
|
|
20136
|
+
chars: "",
|
|
20137
|
+
yield_time_ms: TURN_WRITE_YIELD_MS,
|
|
20138
|
+
max_output_tokens: maxOutputTokens
|
|
20139
|
+
}),
|
|
20140
|
+
void 0
|
|
20141
|
+
);
|
|
20142
|
+
if (typeof next !== "string") {
|
|
20143
|
+
throw new Error("Sandbox lifecycle stdin returned an invalid result");
|
|
20144
|
+
}
|
|
20145
|
+
const nextOutput = execOutput(next);
|
|
20146
|
+
if (nextOutput) {
|
|
20147
|
+
output = appendBoundedOutput3(output, nextOutput, maxOutputTokens);
|
|
20148
|
+
stdout = appendBoundedOutput3(stdout, nextOutput, maxOutputTokens);
|
|
20149
|
+
}
|
|
20150
|
+
const exitCode = parseExecBannerExitCode(next);
|
|
20151
|
+
if (exitCode !== null) {
|
|
20152
|
+
this.shellSessions.delete(sessionId);
|
|
20153
|
+
if (!structured) return completedCommandBanner(exitCode, output);
|
|
20154
|
+
return {
|
|
20155
|
+
stdout,
|
|
20156
|
+
stderr,
|
|
20157
|
+
exitCode,
|
|
20158
|
+
wallTimeSeconds: (performance.now() - startedAt) / 1e3
|
|
20159
|
+
};
|
|
20160
|
+
}
|
|
20161
|
+
await delay(SHELL_POLL_MS);
|
|
20162
|
+
}
|
|
20163
|
+
});
|
|
20164
|
+
}
|
|
20165
|
+
wrapTools(tools, session) {
|
|
20166
|
+
const structuralTools = tools;
|
|
20167
|
+
for (const tool of structuralTools) {
|
|
20168
|
+
if (tool.type !== "function") continue;
|
|
20169
|
+
const functionTool = tool;
|
|
20170
|
+
if (functionTool.name === "exec_command") this.rawExecInvoke = functionTool.invoke;
|
|
20171
|
+
if (functionTool.name === "write_stdin") this.rawWriteInvoke = functionTool.invoke;
|
|
20172
|
+
}
|
|
20173
|
+
return structuralTools.map((tool) => {
|
|
20174
|
+
if (tool.type === "function") return this.wrapFunctionTool(tool, session);
|
|
20175
|
+
if (tool.type === "apply_patch") return this.wrapApplyPatchTool(tool);
|
|
20176
|
+
if (tool.type === "computer") return this.wrapComputerTool(tool);
|
|
20177
|
+
return tool;
|
|
20178
|
+
});
|
|
20179
|
+
}
|
|
20180
|
+
wrapFunctionTool(tool, cancellationSession) {
|
|
20181
|
+
if (tool.name === "exec_command") {
|
|
20182
|
+
return {
|
|
20183
|
+
...tool,
|
|
20184
|
+
invoke: (runContext, input, details) => this.track(async () => {
|
|
20185
|
+
const parsed = parsedObject(input);
|
|
20186
|
+
if (!parsed || typeof parsed.cmd !== "string") {
|
|
20187
|
+
return await this.correlatedInvoke(tool.invoke, runContext, input, details);
|
|
20188
|
+
}
|
|
20189
|
+
const token = crypto.randomUUID();
|
|
20190
|
+
const markerPath = shellMarkerPath(token);
|
|
20191
|
+
const correlationId = details?.toolCall?.callId ?? `turn_exec_${token}`;
|
|
20192
|
+
const cancelExecCommand = cancellationSession?.cancelExecCommand;
|
|
20193
|
+
const useRemoteOpCancellation = Boolean(cancelExecCommand) && cancellationSession?.supportsPty?.() === false;
|
|
20194
|
+
const remoteExec = cancelExecCommand && useRemoteOpCancellation ? this.registerRemoteExec(
|
|
20195
|
+
{ cancelExecCommand: cancelExecCommand.bind(cancellationSession) },
|
|
20196
|
+
`${sanitizeOpIdToken(correlationId)}:0`
|
|
20197
|
+
) : null;
|
|
20198
|
+
const cancellableInput = useRemoteOpCancellation ? input : JSON.stringify({
|
|
20199
|
+
...parsed,
|
|
20200
|
+
cmd: cancellableShellCommand(parsed.cmd, markerPath),
|
|
20201
|
+
// The SDK's only provider-neutral live-process interrupt is a PTY.
|
|
20202
|
+
// The process marker provides TERM/KILL escalation when Ctrl-C is
|
|
20203
|
+
// ignored; the short yield exposes the provider session promptly.
|
|
20204
|
+
tty: true,
|
|
20205
|
+
yield_time_ms: cappedYield(parsed.yield_time_ms, TURN_EXEC_YIELD_MS)
|
|
20206
|
+
});
|
|
20207
|
+
let output;
|
|
20208
|
+
try {
|
|
20209
|
+
output = await runWithToolCallCorrelation(
|
|
20210
|
+
correlationId,
|
|
20211
|
+
async () => await tool.invoke(runContext, cancellableInput, details)
|
|
20212
|
+
);
|
|
20213
|
+
} finally {
|
|
20214
|
+
remoteExec?.settle();
|
|
20215
|
+
}
|
|
20216
|
+
if (typeof output !== "string") return output;
|
|
20217
|
+
const sessionId = useRemoteOpCancellation ? null : parseExecBannerSessionId(output);
|
|
20218
|
+
if (sessionId !== null) {
|
|
20219
|
+
this.shellSessions.set(sessionId, {
|
|
20220
|
+
sessionId,
|
|
20221
|
+
markerPath,
|
|
20222
|
+
token,
|
|
20223
|
+
runContext,
|
|
20224
|
+
execInvoke: tool.invoke,
|
|
20225
|
+
writeInvoke: this.rawWriteInvoke,
|
|
20226
|
+
processSession: retainedProcessSession(cancellationSession, sessionId),
|
|
20227
|
+
identity: null,
|
|
20228
|
+
identityValidated: false,
|
|
20229
|
+
cancellation: null
|
|
20230
|
+
});
|
|
20231
|
+
}
|
|
20232
|
+
return output;
|
|
20233
|
+
})
|
|
20234
|
+
};
|
|
20235
|
+
}
|
|
20236
|
+
if (tool.name === "write_stdin") {
|
|
20237
|
+
return {
|
|
20238
|
+
...tool,
|
|
20239
|
+
invoke: (runContext, input, details) => this.track(async () => {
|
|
20240
|
+
const parsed = parsedObject(input);
|
|
20241
|
+
const sessionId = parsed && typeof parsed.session_id === "number" && Number.isSafeInteger(parsed.session_id) ? parsed.session_id : null;
|
|
20242
|
+
const cappedInput = parsed ? JSON.stringify({
|
|
20243
|
+
...parsed,
|
|
20244
|
+
yield_time_ms: cappedYield(parsed.yield_time_ms, TURN_WRITE_YIELD_MS)
|
|
20245
|
+
}) : input;
|
|
20246
|
+
const directProcessSession = sessionId === null ? null : retainedProcessSession(cancellationSession, sessionId);
|
|
20247
|
+
const output = sessionId !== null && directProcessSession?.writeStdinForProcessMutation ? await directProcessSession.writeStdinForProcessMutation({
|
|
20248
|
+
sessionId,
|
|
20249
|
+
...typeof parsed?.chars === "string" ? { chars: parsed.chars } : {},
|
|
20250
|
+
yieldTimeMs: cappedYield(parsed?.yield_time_ms, TURN_WRITE_YIELD_MS),
|
|
20251
|
+
...typeof parsed?.max_output_tokens === "number" ? { maxOutputTokens: parsed.max_output_tokens } : {}
|
|
20252
|
+
}) : await tool.invoke(runContext, cappedInput, details);
|
|
20253
|
+
if (sessionId !== null && typeof output === "string") {
|
|
20254
|
+
if (isExecSessionLostBanner(output, sessionId)) {
|
|
20255
|
+
this.shellSessions.delete(sessionId);
|
|
20256
|
+
} else if (parseExecBannerSessionId(output) === sessionId) {
|
|
20257
|
+
if (!this.shellSessions.has(sessionId)) {
|
|
20258
|
+
this.shellSessions.set(sessionId, {
|
|
20259
|
+
sessionId,
|
|
20260
|
+
markerPath: null,
|
|
20261
|
+
token: null,
|
|
20262
|
+
runContext,
|
|
20263
|
+
execInvoke: this.rawExecInvoke ?? tool.invoke,
|
|
20264
|
+
writeInvoke: tool.invoke,
|
|
20265
|
+
processSession: directProcessSession,
|
|
20266
|
+
identity: null,
|
|
20267
|
+
identityValidated: false,
|
|
20268
|
+
cancellation: null
|
|
20269
|
+
});
|
|
20270
|
+
}
|
|
20271
|
+
} else if (parseExecBannerExitCode(output) !== null) {
|
|
20272
|
+
this.shellSessions.delete(sessionId);
|
|
20273
|
+
}
|
|
20274
|
+
}
|
|
20275
|
+
return output;
|
|
20276
|
+
})
|
|
20277
|
+
};
|
|
20278
|
+
}
|
|
20279
|
+
return {
|
|
20280
|
+
...tool,
|
|
20281
|
+
invoke: (runContext, input, details) => this.track(async () => await tool.invoke(runContext, input, details))
|
|
20282
|
+
};
|
|
20283
|
+
}
|
|
20284
|
+
wrapApplyPatchTool(tool) {
|
|
20285
|
+
const editor = tool.editor;
|
|
20286
|
+
const wrappedEditor = {
|
|
20287
|
+
createFile: (operation, context) => this.track(async () => await editor.createFile(operation, context)),
|
|
20288
|
+
updateFile: (operation, context) => this.track(async () => await editor.updateFile(operation, context)),
|
|
20289
|
+
deleteFile: (operation, context) => this.track(async () => await editor.deleteFile(operation, context))
|
|
20290
|
+
};
|
|
20291
|
+
return { ...tool, editor: wrappedEditor };
|
|
20292
|
+
}
|
|
20293
|
+
wrapComputerTool(tool) {
|
|
20294
|
+
const computer = tool.computer;
|
|
20295
|
+
if (typeof computer === "function") {
|
|
20296
|
+
const createComputer = computer;
|
|
20297
|
+
return {
|
|
20298
|
+
...tool,
|
|
20299
|
+
computer: async (...args) => wrapComputer(
|
|
20300
|
+
await this.track(async () => await createComputer(...args)),
|
|
20301
|
+
(operation) => this.track(operation)
|
|
20302
|
+
)
|
|
20303
|
+
};
|
|
20304
|
+
}
|
|
20305
|
+
if (computer && typeof computer === "object" && "create" in computer) {
|
|
20306
|
+
const provider = computer;
|
|
20307
|
+
if (typeof provider.create !== "function") return tool;
|
|
20308
|
+
const createComputer = provider.create;
|
|
20309
|
+
return {
|
|
20310
|
+
...tool,
|
|
20311
|
+
computer: {
|
|
20312
|
+
...provider,
|
|
20313
|
+
create: async (...args) => wrapComputer(
|
|
20314
|
+
await this.track(async () => await createComputer(...args)),
|
|
20315
|
+
(operation) => this.track(operation)
|
|
20316
|
+
)
|
|
20317
|
+
}
|
|
20318
|
+
};
|
|
20319
|
+
}
|
|
20320
|
+
if (!computer || typeof computer !== "object") return tool;
|
|
20321
|
+
return {
|
|
20322
|
+
...tool,
|
|
20323
|
+
computer: wrapComputer(computer, (operation) => this.track(operation))
|
|
20324
|
+
};
|
|
20325
|
+
}
|
|
20326
|
+
async correlatedInvoke(invoke, runContext, input, details) {
|
|
20327
|
+
const callId = details?.toolCall?.callId;
|
|
20328
|
+
return callId ? await runWithToolCallCorrelation(
|
|
20329
|
+
callId,
|
|
20330
|
+
async () => await invoke(runContext, input, details)
|
|
20331
|
+
) : await invoke(runContext, input, details);
|
|
20332
|
+
}
|
|
20333
|
+
track(operation) {
|
|
20334
|
+
if (this.cancelled) return Promise.reject(cancellationError(this.reason));
|
|
20335
|
+
const promise = Promise.resolve().then(async () => {
|
|
20336
|
+
if (this.cancelled) throw cancellationError(this.reason);
|
|
20337
|
+
return await operation();
|
|
20338
|
+
});
|
|
20339
|
+
this.inFlight.add(promise);
|
|
20340
|
+
void promise.finally(() => {
|
|
20341
|
+
this.inFlight.delete(promise);
|
|
20342
|
+
}).catch(() => void 0);
|
|
20343
|
+
return promise;
|
|
20344
|
+
}
|
|
20345
|
+
ensureDrain() {
|
|
20346
|
+
this.drainPromise ??= this.drain();
|
|
20347
|
+
return this.drainPromise;
|
|
20348
|
+
}
|
|
20349
|
+
async drain() {
|
|
20350
|
+
while (true) {
|
|
20351
|
+
const remoteCancellations = [...this.remoteExecs].map(async (exec) => await exec.cancel());
|
|
20352
|
+
const inFlight = [...this.inFlight];
|
|
20353
|
+
if (inFlight.length > 0 || remoteCancellations.length > 0) {
|
|
20354
|
+
await Promise.allSettled([...inFlight, ...remoteCancellations]);
|
|
20355
|
+
}
|
|
20356
|
+
const sessions = [...this.shellSessions.values()];
|
|
20357
|
+
if (sessions.length > 0) {
|
|
20358
|
+
await Promise.all(sessions.map(async (session) => await this.cancelShellSession(session)));
|
|
20359
|
+
}
|
|
20360
|
+
if (this.inFlight.size === 0 && this.shellSessions.size === 0 && this.remoteExecs.size === 0)
|
|
20361
|
+
return;
|
|
20362
|
+
}
|
|
20363
|
+
}
|
|
20364
|
+
registerRemoteExec(session, opId) {
|
|
20365
|
+
let resolveSettled;
|
|
20366
|
+
const settledPromise = new Promise((resolve5) => {
|
|
20367
|
+
resolveSettled = resolve5;
|
|
20368
|
+
});
|
|
20369
|
+
let cancellation = null;
|
|
20370
|
+
const entry = {
|
|
20371
|
+
settled: false,
|
|
20372
|
+
settledPromise,
|
|
20373
|
+
settle: () => {
|
|
20374
|
+
if (entry.settled) return;
|
|
20375
|
+
entry.settled = true;
|
|
20376
|
+
resolveSettled();
|
|
20377
|
+
this.remoteExecs.delete(entry);
|
|
20378
|
+
},
|
|
20379
|
+
cancel: () => {
|
|
20380
|
+
cancellation ??= (async () => {
|
|
20381
|
+
while (!entry.settled) {
|
|
20382
|
+
const attempt = session.cancelExecCommand(opId).catch(() => false);
|
|
20383
|
+
await Promise.race([attempt, entry.settledPromise]);
|
|
20384
|
+
await Promise.race([entry.settledPromise, delay(SHELL_POLL_MS)]);
|
|
20385
|
+
}
|
|
20386
|
+
})();
|
|
20387
|
+
return cancellation;
|
|
20388
|
+
}
|
|
20389
|
+
};
|
|
20390
|
+
this.remoteExecs.add(entry);
|
|
20391
|
+
return entry;
|
|
20392
|
+
}
|
|
20393
|
+
cancelShellSession(state) {
|
|
20394
|
+
state.cancellation ??= this.cancelShellSessionOnce(state).finally(() => {
|
|
20395
|
+
state.cancellation = null;
|
|
20396
|
+
});
|
|
20397
|
+
return state.cancellation;
|
|
20398
|
+
}
|
|
20399
|
+
async cancelShellSessionOnce(state) {
|
|
20400
|
+
if (!state.markerPath) {
|
|
20401
|
+
while (true) {
|
|
20402
|
+
if ((state.writeInvoke || state.processSession?.writeStdinForProcessControl) && await this.rawWrite(state, "", SHELL_POLL_MS)) {
|
|
20403
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
20404
|
+
return;
|
|
20405
|
+
}
|
|
20406
|
+
await delay(SHELL_POLL_MS);
|
|
20407
|
+
}
|
|
20408
|
+
}
|
|
20409
|
+
while (!state.identity) {
|
|
20410
|
+
state.identity = await this.readIdentity(state);
|
|
20411
|
+
if (state.identity) break;
|
|
20412
|
+
if (state.writeInvoke || state.processSession?.writeStdinForProcessControl) {
|
|
20413
|
+
const completed = await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
20414
|
+
if (completed) {
|
|
20415
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
20416
|
+
return;
|
|
20417
|
+
}
|
|
20418
|
+
}
|
|
20419
|
+
await delay(SHELL_POLL_MS);
|
|
20420
|
+
}
|
|
20421
|
+
if (!await this.identityAlive(state)) {
|
|
20422
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
20423
|
+
return;
|
|
20424
|
+
}
|
|
20425
|
+
if (!state.processSession && state.writeInvoke) {
|
|
20426
|
+
await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
20427
|
+
if (!await this.identityAlive(state)) {
|
|
20428
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
20429
|
+
return;
|
|
20430
|
+
}
|
|
20431
|
+
}
|
|
20432
|
+
await this.signalIdentity(state, "TERM");
|
|
20433
|
+
for (let attempt = 0; attempt < SHELL_GRACEFUL_POLLS; attempt++) {
|
|
20434
|
+
if (!state.processSession && state.writeInvoke) {
|
|
20435
|
+
await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
20436
|
+
}
|
|
20437
|
+
if (!await this.identityAlive(state)) {
|
|
20438
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
20439
|
+
return;
|
|
20440
|
+
}
|
|
20441
|
+
}
|
|
20442
|
+
await this.signalIdentity(state, "KILL");
|
|
20443
|
+
while (await this.identityAlive(state)) {
|
|
20444
|
+
if (!state.processSession && state.writeInvoke) {
|
|
20445
|
+
await this.rawWrite(state, "", SHELL_POLL_MS);
|
|
20446
|
+
}
|
|
20447
|
+
await this.signalIdentity(state, "KILL");
|
|
20448
|
+
await delay(SHELL_POLL_MS);
|
|
20449
|
+
}
|
|
20450
|
+
await this.forgetShellSessionAfterExactSettlement(state);
|
|
20451
|
+
}
|
|
20452
|
+
async forgetShellSessionAfterExactSettlement(state) {
|
|
20453
|
+
while (state.processSession?.hasRetainedProcess?.(state.sessionId) === true) {
|
|
20454
|
+
if (await this.rawWrite(state, "", SHELL_POLL_MS)) break;
|
|
20455
|
+
await delay(SHELL_POLL_MS);
|
|
20456
|
+
}
|
|
20457
|
+
this.shellSessions.delete(state.sessionId);
|
|
20458
|
+
}
|
|
20459
|
+
async rawWrite(state, chars, yieldTimeMs) {
|
|
20460
|
+
if (!state.writeInvoke && !state.processSession?.writeStdinForProcessControl) return false;
|
|
20461
|
+
try {
|
|
20462
|
+
const output = state.processSession?.writeStdinForProcessControl ? await state.processSession.writeStdinForProcessControl({
|
|
20463
|
+
sessionId: state.sessionId,
|
|
20464
|
+
chars,
|
|
20465
|
+
yieldTimeMs,
|
|
20466
|
+
maxOutputTokens: 128
|
|
20467
|
+
}) : await state.writeInvoke(
|
|
20468
|
+
state.runContext,
|
|
20469
|
+
JSON.stringify({
|
|
20470
|
+
session_id: state.sessionId,
|
|
20471
|
+
chars,
|
|
20472
|
+
yield_time_ms: yieldTimeMs,
|
|
20473
|
+
max_output_tokens: 128
|
|
20474
|
+
}),
|
|
20475
|
+
void 0
|
|
20476
|
+
);
|
|
20477
|
+
if (typeof output !== "string") return false;
|
|
20478
|
+
if (isExecSessionLostBanner(output, state.sessionId)) return true;
|
|
20479
|
+
if (parseExecBannerSessionId(output) === state.sessionId) return false;
|
|
20480
|
+
if (parseExecBannerExitCode(output) !== null) return true;
|
|
20481
|
+
} catch {
|
|
20482
|
+
}
|
|
20483
|
+
return false;
|
|
20484
|
+
}
|
|
20485
|
+
async readIdentity(state) {
|
|
20486
|
+
if (!state.markerPath) return null;
|
|
20487
|
+
try {
|
|
20488
|
+
const command = `command cat ${singleQuote(state.markerPath)} 2>/dev/null`;
|
|
20489
|
+
const output = state.processSession?.execCommandForProcessControl ? await state.processSession.execCommandForProcessControl(
|
|
20490
|
+
state.sessionId,
|
|
20491
|
+
shellHelperArgs(command)
|
|
20492
|
+
) : await state.execInvoke(state.runContext, shellHelperInput(command), void 0);
|
|
20493
|
+
return typeof output === "string" ? safeProcessIdentity(output) : null;
|
|
20494
|
+
} catch {
|
|
20495
|
+
return null;
|
|
20496
|
+
}
|
|
20497
|
+
}
|
|
20498
|
+
async identityAlive(state) {
|
|
20499
|
+
const script = state.identityValidated ? [
|
|
20500
|
+
`__opengeni_pgid=${state.identity?.processGroupId ?? 0}`,
|
|
20501
|
+
'command kill -0 "-$__opengeni_pgid" 2>/dev/null && exit 75',
|
|
20502
|
+
"exit 0"
|
|
20503
|
+
].join("\n") : identityGuardScript(
|
|
20504
|
+
state,
|
|
20505
|
+
'command kill -0 "-$__opengeni_pgid" 2>/dev/null && exit 75\nexit 0',
|
|
20506
|
+
76
|
|
20507
|
+
);
|
|
20508
|
+
try {
|
|
20509
|
+
const output = state.processSession?.execCommandForProcessControl ? await state.processSession.execCommandForProcessControl(
|
|
20510
|
+
state.sessionId,
|
|
20511
|
+
shellHelperArgs(script)
|
|
20512
|
+
) : await state.execInvoke(state.runContext, shellHelperInput(script), void 0);
|
|
20513
|
+
if (typeof output !== "string") return true;
|
|
20514
|
+
const exitCode = parseExecBannerExitCode(output);
|
|
20515
|
+
if (exitCode === 75) {
|
|
20516
|
+
state.identityValidated = true;
|
|
20517
|
+
return true;
|
|
20518
|
+
}
|
|
20519
|
+
return exitCode !== 0;
|
|
20520
|
+
} catch {
|
|
20521
|
+
return true;
|
|
20522
|
+
}
|
|
20523
|
+
}
|
|
20524
|
+
async signalIdentity(state, signal) {
|
|
20525
|
+
const script = state.identityValidated ? `command kill -${signal} "-${state.identity?.processGroupId ?? 0}" 2>/dev/null || exit 76` : identityGuardScript(
|
|
20526
|
+
state,
|
|
20527
|
+
`command kill -${signal} "-$__opengeni_pgid" 2>/dev/null || exit 76
|
|
20528
|
+
exit 0`,
|
|
20529
|
+
76
|
|
20530
|
+
);
|
|
20531
|
+
try {
|
|
20532
|
+
if (state.processSession?.execCommandForProcessControl) {
|
|
20533
|
+
await state.processSession.execCommandForProcessControl(
|
|
20534
|
+
state.sessionId,
|
|
20535
|
+
shellHelperArgs(script)
|
|
20536
|
+
);
|
|
20537
|
+
} else {
|
|
20538
|
+
await state.execInvoke(state.runContext, shellHelperInput(script), void 0);
|
|
20539
|
+
}
|
|
20540
|
+
} catch {
|
|
20541
|
+
}
|
|
20542
|
+
}
|
|
20543
|
+
};
|
|
20544
|
+
function createTurnToolCancellationController(signal) {
|
|
20545
|
+
return new TurnToolCancellationControllerImpl(signal);
|
|
19453
20546
|
}
|
|
19454
|
-
function
|
|
19455
|
-
|
|
20547
|
+
function wrapCapabilityToolsForTurnCancellation(capability, controller) {
|
|
20548
|
+
const originalTools = capability.tools;
|
|
20549
|
+
capability.tools = function() {
|
|
20550
|
+
return controller.wrapTools(originalTools.call(this), this._session);
|
|
20551
|
+
};
|
|
19456
20552
|
}
|
|
19457
20553
|
|
|
19458
20554
|
// src/sandbox/channel-a.ts
|
|
@@ -20273,17 +21369,49 @@ var SandboxChannelAService = class {
|
|
|
20273
21369
|
return (await this.detectReposDetailed()).repos;
|
|
20274
21370
|
}
|
|
20275
21371
|
// ════════════════════════ Terminal exec + PTY (A2) ════════════════════════
|
|
20276
|
-
/** Run a bounded command
|
|
20277
|
-
*
|
|
20278
|
-
*
|
|
20279
|
-
* the agent firehose so other viewers see it. */
|
|
21372
|
+
/** Run a bounded command to physical completion and return buffered output.
|
|
21373
|
+
* A deadline cancels the exact provider operation, proves its process group
|
|
21374
|
+
* absent, and settles any retained-process admission before returning 124. */
|
|
20280
21375
|
async terminalExec(req) {
|
|
20281
|
-
const
|
|
20282
|
-
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
|
|
21376
|
+
const abort = new AbortController();
|
|
21377
|
+
const controller = createTurnToolCancellationController(abort.signal);
|
|
21378
|
+
const timeoutReason = new Error(`Terminal command exceeded ${req.timeoutMs}ms`);
|
|
21379
|
+
let timedOut = false;
|
|
21380
|
+
const timer = setTimeout(() => {
|
|
21381
|
+
timedOut = true;
|
|
21382
|
+
abort.abort(timeoutReason);
|
|
21383
|
+
}, req.timeoutMs);
|
|
21384
|
+
const terminalWorkdir = this.terminalWorkdir(req.cwd);
|
|
21385
|
+
let r;
|
|
21386
|
+
try {
|
|
21387
|
+
r = await controller.runSandboxCommandStructured(this.session, {
|
|
21388
|
+
cmd: req.command,
|
|
21389
|
+
...terminalWorkdir ? { workdir: terminalWorkdir } : {},
|
|
21390
|
+
...this.runAs ? { runAs: this.runAs } : {},
|
|
21391
|
+
yieldTimeMs: req.timeoutMs
|
|
21392
|
+
});
|
|
21393
|
+
if (timedOut) {
|
|
21394
|
+
await controller.waitForQuiescence();
|
|
21395
|
+
r = {
|
|
21396
|
+
stdout: "",
|
|
21397
|
+
stderr: timeoutReason.message,
|
|
21398
|
+
exitCode: 124,
|
|
21399
|
+
wallTimeSeconds: req.timeoutMs / 1e3
|
|
21400
|
+
};
|
|
21401
|
+
}
|
|
21402
|
+
} catch (error) {
|
|
21403
|
+
controller.cancel(error);
|
|
21404
|
+
await controller.waitForQuiescence();
|
|
21405
|
+
if (!timedOut && !(error instanceof TurnSandboxCommandCancelledError)) throw error;
|
|
21406
|
+
r = {
|
|
21407
|
+
stdout: "",
|
|
21408
|
+
stderr: timeoutReason.message,
|
|
21409
|
+
exitCode: 124,
|
|
21410
|
+
wallTimeSeconds: req.timeoutMs / 1e3
|
|
21411
|
+
};
|
|
21412
|
+
} finally {
|
|
21413
|
+
clearTimeout(timer);
|
|
21414
|
+
}
|
|
20287
21415
|
if (req.emitStream && (r.stdout || r.stderr)) {
|
|
20288
21416
|
const events = [];
|
|
20289
21417
|
const commandId = crypto.randomUUID();
|
|
@@ -20303,7 +21431,7 @@ var SandboxChannelAService = class {
|
|
|
20303
21431
|
stdout: r.stdout,
|
|
20304
21432
|
stderr: r.stderr,
|
|
20305
21433
|
exitCode: r.exitCode,
|
|
20306
|
-
running,
|
|
21434
|
+
running: false,
|
|
20307
21435
|
wallTimeSeconds: r.wallTimeSeconds
|
|
20308
21436
|
};
|
|
20309
21437
|
}
|
|
@@ -20333,10 +21461,11 @@ var SandboxChannelAService = class {
|
|
|
20333
21461
|
* it as terminal.pty.output.delta). Throws ChannelAUnsupportedError when the
|
|
20334
21462
|
* backend has no writeStdin. */
|
|
20335
21463
|
async ptyWrite(_req, execSessionId, data) {
|
|
20336
|
-
|
|
21464
|
+
const write = this.session.writeStdinForProcessMutation?.bind(this.session) ?? this.session.writeStdin?.bind(this.session);
|
|
21465
|
+
if (!write) {
|
|
20337
21466
|
throw new ChannelAUnsupportedError("interactive terminal unsupported on this backend");
|
|
20338
21467
|
}
|
|
20339
|
-
const out = await
|
|
21468
|
+
const out = await write({
|
|
20340
21469
|
sessionId: execSessionId,
|
|
20341
21470
|
chars: data,
|
|
20342
21471
|
yieldTimeMs: 250
|
|
@@ -20349,23 +21478,22 @@ var SandboxChannelAService = class {
|
|
|
20349
21478
|
/** Resize an open PTY (SIGWINCH via stty against the exec-session). The SDK has
|
|
20350
21479
|
* no resize method; stty in the same tty session updates the geometry. */
|
|
20351
21480
|
async ptyResize(req, execSessionId) {
|
|
20352
|
-
|
|
20353
|
-
|
|
21481
|
+
const write = this.session.writeStdinForProcessControl?.bind(this.session) ?? this.session.writeStdin?.bind(this.session);
|
|
21482
|
+
if (!write) return;
|
|
21483
|
+
await write({
|
|
20354
21484
|
sessionId: execSessionId,
|
|
20355
21485
|
chars: `stty cols ${req.cols} rows ${req.rows}
|
|
20356
21486
|
`,
|
|
20357
21487
|
yieldTimeMs: 50
|
|
20358
21488
|
});
|
|
20359
21489
|
}
|
|
20360
|
-
/**
|
|
20361
|
-
*
|
|
21490
|
+
/** Ask an open PTY to exit and return the exact provider banner. The routing
|
|
21491
|
+
* session settles durable process authority only when that banner proves this
|
|
21492
|
+
* locator exited or was lost; callers must keep metadata open otherwise. */
|
|
20362
21493
|
async ptyClose(_req, execSessionId) {
|
|
20363
|
-
|
|
20364
|
-
|
|
20365
|
-
|
|
20366
|
-
} catch {
|
|
20367
|
-
}
|
|
20368
|
-
}
|
|
21494
|
+
const write = this.session.writeStdinForProcessControl?.bind(this.session) ?? this.session.writeStdin?.bind(this.session);
|
|
21495
|
+
if (execSessionId === null || !write) return "";
|
|
21496
|
+
return await write({ sessionId: execSessionId, chars: "", yieldTimeMs: 250 });
|
|
20369
21497
|
}
|
|
20370
21498
|
// ──────────────────────────── helpers ──────────────────────────────────────
|
|
20371
21499
|
/** The current FS revision (for the caller to persist/seed). */
|
|
@@ -20497,12 +21625,6 @@ function isDefinitePathNotFoundError(error) {
|
|
|
20497
21625
|
const candidate = error;
|
|
20498
21626
|
return candidate.osNotFound === true || candidate.code === "ENOENT" || candidate.errno === "ENOENT" || candidate.errno === -2;
|
|
20499
21627
|
}
|
|
20500
|
-
function isExecSessionLostBanner(out, execSessionId) {
|
|
20501
|
-
if (!out || !Number.isSafeInteger(execSessionId) || execSessionId < 0) return false;
|
|
20502
|
-
const match = out.match(/^(?:write_stdin failed: )?session not found: (\d+)$/);
|
|
20503
|
-
if (!match || match[0] !== out) return false;
|
|
20504
|
-
return match[1] === String(execSessionId);
|
|
20505
|
-
}
|
|
20506
21628
|
function sniffBinary(bytes) {
|
|
20507
21629
|
const n = Math.min(bytes.byteLength, 8192);
|
|
20508
21630
|
for (let i = 0; i < n; i++) if (bytes[i] === 0) return true;
|
|
@@ -21099,6 +22221,35 @@ var RoutingUnsupportedError = class extends Error {
|
|
|
21099
22221
|
super(`the active sandbox (${kind}) does not support "${op}"`);
|
|
21100
22222
|
}
|
|
21101
22223
|
};
|
|
22224
|
+
var RoutingBackendRecoveryRequiredError = class extends Error {
|
|
22225
|
+
constructor(op, leaseEpoch, recovery) {
|
|
22226
|
+
super(
|
|
22227
|
+
`sandbox backend disappeared during ${op}; recovery is ${recovery} at epoch ${leaseEpoch}`
|
|
22228
|
+
);
|
|
22229
|
+
this.op = op;
|
|
22230
|
+
this.leaseEpoch = leaseEpoch;
|
|
22231
|
+
this.recovery = recovery;
|
|
22232
|
+
this.retryable = recovery === "pending" || recovery === "superseded";
|
|
22233
|
+
}
|
|
22234
|
+
name = "RoutingBackendRecoveryRequiredError";
|
|
22235
|
+
retryable;
|
|
22236
|
+
};
|
|
22237
|
+
var RoutingMutationOutcomeUnknownError = class extends Error {
|
|
22238
|
+
constructor(op, message, options) {
|
|
22239
|
+
super(message, options);
|
|
22240
|
+
this.op = op;
|
|
22241
|
+
}
|
|
22242
|
+
name = "RoutingMutationOutcomeUnknownError";
|
|
22243
|
+
retryable = false;
|
|
22244
|
+
};
|
|
22245
|
+
var RoutingRetainedProcessNotFoundError = class extends Error {
|
|
22246
|
+
constructor(providerSessionId) {
|
|
22247
|
+
super(`retained sandbox process ${providerSessionId} is not tracked on its original route`);
|
|
22248
|
+
this.providerSessionId = providerSessionId;
|
|
22249
|
+
}
|
|
22250
|
+
name = "RoutingRetainedProcessNotFoundError";
|
|
22251
|
+
retryable = false;
|
|
22252
|
+
};
|
|
21102
22253
|
function isFenceError(error) {
|
|
21103
22254
|
if (!error || typeof error !== "object") {
|
|
21104
22255
|
return false;
|
|
@@ -21111,6 +22262,59 @@ function isFenceError(error) {
|
|
|
21111
22262
|
const haystack = `${name} ${message}`.toLowerCase();
|
|
21112
22263
|
return haystack.includes("fenced") || haystack.includes("epoch") && haystack.includes("super");
|
|
21113
22264
|
}
|
|
22265
|
+
function positiveProviderSessionId(value) {
|
|
22266
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
22267
|
+
}
|
|
22268
|
+
function canonicalUuid(value) {
|
|
22269
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
22270
|
+
}
|
|
22271
|
+
function providerSessionIdFromResult(result) {
|
|
22272
|
+
if (typeof result === "string") {
|
|
22273
|
+
return positiveProviderSessionId(parseExecBannerSessionId(result));
|
|
22274
|
+
}
|
|
22275
|
+
if (!result || typeof result !== "object") return null;
|
|
22276
|
+
const record = result;
|
|
22277
|
+
return positiveProviderSessionId(record.sessionId) ?? positiveProviderSessionId(record.session_id);
|
|
22278
|
+
}
|
|
22279
|
+
function providerSessionIdFromArgs(args) {
|
|
22280
|
+
if (!args || typeof args !== "object") return null;
|
|
22281
|
+
const record = args;
|
|
22282
|
+
return positiveProviderSessionId(record.sessionId) ?? positiveProviderSessionId(record.session_id);
|
|
22283
|
+
}
|
|
22284
|
+
function retainedProcessTerminalProof(result, providerSessionId) {
|
|
22285
|
+
if (isExecSessionLostBanner(result, providerSessionId)) {
|
|
22286
|
+
return {
|
|
22287
|
+
outcome: "lost",
|
|
22288
|
+
exitCode: null,
|
|
22289
|
+
reason: "provider_session_lost_banner"
|
|
22290
|
+
};
|
|
22291
|
+
}
|
|
22292
|
+
const exitCode = parseExecBannerExitCode(result);
|
|
22293
|
+
return exitCode === null ? null : { outcome: "exited", exitCode, reason: "provider_exit_banner" };
|
|
22294
|
+
}
|
|
22295
|
+
function formatExecResult(result) {
|
|
22296
|
+
if (typeof result === "string") return result;
|
|
22297
|
+
if (!result || typeof result !== "object") {
|
|
22298
|
+
throw new Error("sandbox process-control exec returned an invalid result");
|
|
22299
|
+
}
|
|
22300
|
+
const record = result;
|
|
22301
|
+
const output = [record.output, record.stderr, record.stdout].filter((value) => typeof value === "string" && value.length > 0).join("\n");
|
|
22302
|
+
const sessionId = positiveProviderSessionId(record.sessionId) ?? positiveProviderSessionId(record.session_id);
|
|
22303
|
+
if (sessionId !== null) {
|
|
22304
|
+
return `Process running with session ID ${sessionId}
|
|
22305
|
+
|
|
22306
|
+
Output:
|
|
22307
|
+
${output}`;
|
|
22308
|
+
}
|
|
22309
|
+
const exitCode = typeof record.exitCode === "number" ? record.exitCode : typeof record.exit_code === "number" ? record.exit_code : null;
|
|
22310
|
+
if (exitCode !== null && Number.isSafeInteger(exitCode)) {
|
|
22311
|
+
return `Process exited with code ${exitCode}
|
|
22312
|
+
|
|
22313
|
+
Output:
|
|
22314
|
+
${output}`;
|
|
22315
|
+
}
|
|
22316
|
+
throw new Error("sandbox process-control exec reported neither session id nor exit code");
|
|
22317
|
+
}
|
|
21114
22318
|
var RoutingSandboxSession = class {
|
|
21115
22319
|
deps;
|
|
21116
22320
|
maxFenceRetries;
|
|
@@ -21129,6 +22333,10 @@ var RoutingSandboxSession = class {
|
|
|
21129
22333
|
// The last-resolved backend, exposed via the `state` getter (a method-free read
|
|
21130
22334
|
// of the active backend's `state`). Updated on every resolve.
|
|
21131
22335
|
lastResolved;
|
|
22336
|
+
/** Provider session ids are scoped to one backend instance. Each entry copies
|
|
22337
|
+
* that exact resolved route so pointer movement can never redirect stdin,
|
|
22338
|
+
* polling, or process-group helpers to another box. */
|
|
22339
|
+
retainedProcesses = /* @__PURE__ */ new Map();
|
|
21132
22340
|
// The native-desktop control-plane ops (self-hosted / macOS). Declared as OPTIONAL
|
|
21133
22341
|
// INSTANCE fields — NOT prototype methods — because their PRESENCE is the selection
|
|
21134
22342
|
// signal `isNativeDesktopSession` (sandbox-computer.ts) uses to pick the native vs
|
|
@@ -21144,13 +22352,13 @@ var RoutingSandboxSession = class {
|
|
|
21144
22352
|
this.maxFenceRetries = deps.maxFenceRetries ?? 3;
|
|
21145
22353
|
const def = deps.defaultResolved?.session;
|
|
21146
22354
|
if (typeof def?.desktopInput === "function" && typeof def?.screenshot === "function") {
|
|
21147
|
-
this.desktopInput = (event) => this.dispatch("desktopInput", async (s) => {
|
|
22355
|
+
this.desktopInput = (event) => this.dispatch("desktopInput", true, async (s) => {
|
|
21148
22356
|
if (!s.desktopInput) {
|
|
21149
22357
|
throw new RoutingUnsupportedError("desktopInput", this.cached?.kind ?? "unknown");
|
|
21150
22358
|
}
|
|
21151
22359
|
return s.desktopInput(event);
|
|
21152
22360
|
});
|
|
21153
|
-
this.screenshot = () => this.dispatch("screenshot", async (s) => {
|
|
22361
|
+
this.screenshot = () => this.dispatch("screenshot", false, async (s) => {
|
|
21154
22362
|
if (!s.screenshot) {
|
|
21155
22363
|
throw new RoutingUnsupportedError("screenshot", this.cached?.kind ?? "unknown");
|
|
21156
22364
|
}
|
|
@@ -21195,18 +22403,256 @@ var RoutingSandboxSession = class {
|
|
|
21195
22403
|
"RoutingSandboxSession.resolveActiveBackend returned the proxy itself as the active backend (re-entrancy) \u2014 the resolver must return the underlying box session, not the routing proxy."
|
|
21196
22404
|
);
|
|
21197
22405
|
}
|
|
22406
|
+
const routed = {
|
|
22407
|
+
...resolved,
|
|
22408
|
+
activeEpoch: pointer.activeEpoch
|
|
22409
|
+
};
|
|
21198
22410
|
this.cachedEpoch = pointer.activeEpoch;
|
|
21199
22411
|
this.cachedSandboxId = pointer.activeSandboxId;
|
|
21200
|
-
this.cached =
|
|
21201
|
-
this.lastResolved =
|
|
22412
|
+
this.cached = routed;
|
|
22413
|
+
this.lastResolved = routed;
|
|
21202
22414
|
this.deps.onTransition?.({
|
|
21203
22415
|
type: this.cachedEpoch !== void 0 && fromEpoch !== pointer.activeEpoch ? "epoch-changed" : "resolved",
|
|
21204
22416
|
fromEpoch,
|
|
21205
22417
|
toEpoch: pointer.activeEpoch,
|
|
21206
|
-
sandboxId:
|
|
21207
|
-
kind:
|
|
22418
|
+
sandboxId: routed.sandboxId,
|
|
22419
|
+
kind: routed.kind
|
|
21208
22420
|
});
|
|
21209
|
-
return
|
|
22421
|
+
return routed;
|
|
22422
|
+
}
|
|
22423
|
+
invalidate(backend) {
|
|
22424
|
+
this.cachedEpoch = void 0;
|
|
22425
|
+
this.cachedSandboxId = void 0;
|
|
22426
|
+
this.cached = void 0;
|
|
22427
|
+
this.deps.onTransition?.({
|
|
22428
|
+
type: "fenced-retry",
|
|
22429
|
+
fromEpoch: backend.activeEpoch ?? 0,
|
|
22430
|
+
toEpoch: 0,
|
|
22431
|
+
sandboxId: backend.sandboxId,
|
|
22432
|
+
kind: backend.kind
|
|
22433
|
+
});
|
|
22434
|
+
}
|
|
22435
|
+
registerRetainedProcess(process2, backend) {
|
|
22436
|
+
if (this.retainedProcesses.has(process2.providerSessionId)) {
|
|
22437
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22438
|
+
"retainProcess",
|
|
22439
|
+
`Provider session ${process2.providerSessionId} was yielded while that locator was already retained; neither process was rebound`
|
|
22440
|
+
);
|
|
22441
|
+
}
|
|
22442
|
+
const record = {
|
|
22443
|
+
process: process2,
|
|
22444
|
+
backend,
|
|
22445
|
+
durable: !this.deps.afterMutation,
|
|
22446
|
+
pendingParentPromotion: null,
|
|
22447
|
+
pendingMutationSettlement: null,
|
|
22448
|
+
pendingTerminal: null,
|
|
22449
|
+
settlement: null
|
|
22450
|
+
};
|
|
22451
|
+
this.retainedProcesses.set(process2.providerSessionId, record);
|
|
22452
|
+
return record;
|
|
22453
|
+
}
|
|
22454
|
+
retainedProcess(providerSessionId) {
|
|
22455
|
+
const retained = this.retainedProcesses.get(providerSessionId);
|
|
22456
|
+
if (!retained) throw new RoutingRetainedProcessNotFoundError(providerSessionId);
|
|
22457
|
+
return retained;
|
|
22458
|
+
}
|
|
22459
|
+
/** Reconstruct one process that was already promoted durably by an earlier
|
|
22460
|
+
* request. Adoption never reads the active pointer and never repeats parent
|
|
22461
|
+
* promotion. It can bind only to the exact construction-time default backend;
|
|
22462
|
+
* callers must reject non-default routes until they can independently rebuild
|
|
22463
|
+
* that provider session from its copied durable identity. */
|
|
22464
|
+
adoptRetainedProcess(input) {
|
|
22465
|
+
const providerSessionId = positiveProviderSessionId(input.process.providerSessionId);
|
|
22466
|
+
const seed = this.deps.defaultResolved;
|
|
22467
|
+
if (providerSessionId === null || !canonicalUuid(input.process.id) || !seed || seed.sandboxId !== input.backend.sandboxId || seed.leaseEpoch !== input.backend.leaseEpoch || seed.providerInstanceId !== input.backend.providerInstanceId || !Number.isSafeInteger(input.backend.activeEpoch) || input.backend.activeEpoch < 0) {
|
|
22468
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22469
|
+
"adoptRetainedProcess",
|
|
22470
|
+
`Retained provider session ${input.process.providerSessionId} did not match the exact default backend identity`
|
|
22471
|
+
);
|
|
22472
|
+
}
|
|
22473
|
+
const existing = this.retainedProcesses.get(providerSessionId);
|
|
22474
|
+
if (existing) {
|
|
22475
|
+
if (existing.process.id === input.process.id && existing.backend.session === seed.session && existing.backend.sandboxId === input.backend.sandboxId && existing.backend.leaseEpoch === input.backend.leaseEpoch && existing.backend.providerInstanceId === input.backend.providerInstanceId && existing.backend.activeEpoch === input.backend.activeEpoch) {
|
|
22476
|
+
return;
|
|
22477
|
+
}
|
|
22478
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22479
|
+
"adoptRetainedProcess",
|
|
22480
|
+
`Provider session ${providerSessionId} is already bound to a different retained process identity`
|
|
22481
|
+
);
|
|
22482
|
+
}
|
|
22483
|
+
this.retainedProcesses.set(providerSessionId, {
|
|
22484
|
+
process: { ...input.process },
|
|
22485
|
+
backend: {
|
|
22486
|
+
...seed,
|
|
22487
|
+
activeEpoch: input.backend.activeEpoch
|
|
22488
|
+
},
|
|
22489
|
+
durable: true,
|
|
22490
|
+
pendingParentPromotion: null,
|
|
22491
|
+
pendingMutationSettlement: null,
|
|
22492
|
+
pendingTerminal: null,
|
|
22493
|
+
settlement: null
|
|
22494
|
+
});
|
|
22495
|
+
}
|
|
22496
|
+
async ensureParentPromotion(record) {
|
|
22497
|
+
if (record.durable) return;
|
|
22498
|
+
const pending = record.pendingParentPromotion;
|
|
22499
|
+
if (!pending || !this.deps.afterMutation) {
|
|
22500
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22501
|
+
"retainProcess",
|
|
22502
|
+
`Retained provider session ${record.process.providerSessionId} has no confirmed durable parent admission`
|
|
22503
|
+
);
|
|
22504
|
+
}
|
|
22505
|
+
try {
|
|
22506
|
+
await this.deps.afterMutation(pending);
|
|
22507
|
+
record.pendingParentPromotion = null;
|
|
22508
|
+
record.durable = true;
|
|
22509
|
+
} catch (error) {
|
|
22510
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22511
|
+
pending.op,
|
|
22512
|
+
`Yielded sandbox process ${record.process.providerSessionId} could not confirm its durable promotion; tracking remains pinned to the original backend`,
|
|
22513
|
+
{ cause: error }
|
|
22514
|
+
);
|
|
22515
|
+
}
|
|
22516
|
+
}
|
|
22517
|
+
async settleRetainedProcess(record, proof, result) {
|
|
22518
|
+
record.pendingTerminal ??= { proof, result };
|
|
22519
|
+
const pending = record.pendingTerminal;
|
|
22520
|
+
if (pending.proof.outcome !== proof.outcome || pending.proof.exitCode !== proof.exitCode || pending.proof.reason !== proof.reason) {
|
|
22521
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22522
|
+
"settleProcess",
|
|
22523
|
+
`Retained provider session ${record.process.providerSessionId} produced conflicting terminal proof`
|
|
22524
|
+
);
|
|
22525
|
+
}
|
|
22526
|
+
record.settlement ??= (async () => {
|
|
22527
|
+
await this.ensureParentPromotion(record);
|
|
22528
|
+
await this.deps.settleProcess?.({
|
|
22529
|
+
backend: record.backend,
|
|
22530
|
+
process: record.process,
|
|
22531
|
+
proof: pending.proof
|
|
22532
|
+
});
|
|
22533
|
+
this.retainedProcesses.delete(record.process.providerSessionId);
|
|
22534
|
+
record.pendingTerminal = null;
|
|
22535
|
+
})();
|
|
22536
|
+
try {
|
|
22537
|
+
await record.settlement;
|
|
22538
|
+
} catch (error) {
|
|
22539
|
+
record.settlement = null;
|
|
22540
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22541
|
+
"settleProcess",
|
|
22542
|
+
`Retained provider session ${record.process.providerSessionId} reached a terminal state but durable settlement failed; tracking remains pinned`,
|
|
22543
|
+
{ cause: error }
|
|
22544
|
+
);
|
|
22545
|
+
}
|
|
22546
|
+
}
|
|
22547
|
+
async flushPendingProcessMutation(record) {
|
|
22548
|
+
const pending = record.pendingMutationSettlement;
|
|
22549
|
+
if (!pending) return null;
|
|
22550
|
+
try {
|
|
22551
|
+
await this.deps.afterProcessMutation?.(pending);
|
|
22552
|
+
record.pendingMutationSettlement = null;
|
|
22553
|
+
} catch (error) {
|
|
22554
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22555
|
+
pending.op,
|
|
22556
|
+
`Retained-process mutation ${pending.op} still lacks durable physical settlement; no later process mutation was admitted`,
|
|
22557
|
+
{ cause: error }
|
|
22558
|
+
);
|
|
22559
|
+
}
|
|
22560
|
+
if (pending.outcome === "resolved" && typeof pending.result === "string") {
|
|
22561
|
+
const proof = retainedProcessTerminalProof(pending.result, record.process.providerSessionId);
|
|
22562
|
+
if (proof) {
|
|
22563
|
+
await this.settleRetainedProcess(record, proof, pending.result);
|
|
22564
|
+
return pending.result;
|
|
22565
|
+
}
|
|
22566
|
+
}
|
|
22567
|
+
return null;
|
|
22568
|
+
}
|
|
22569
|
+
async dispatchProcessMutation(args) {
|
|
22570
|
+
const providerSessionId = providerSessionIdFromArgs(args);
|
|
22571
|
+
if (providerSessionId === null) throw new RoutingRetainedProcessNotFoundError(-1);
|
|
22572
|
+
const record = this.retainedProcess(providerSessionId);
|
|
22573
|
+
const priorTerminal = await this.flushPendingProcessMutation(record);
|
|
22574
|
+
if (priorTerminal !== null) return priorTerminal;
|
|
22575
|
+
if (record.pendingTerminal) {
|
|
22576
|
+
const terminal = record.pendingTerminal;
|
|
22577
|
+
await this.settleRetainedProcess(record, terminal.proof, terminal.result);
|
|
22578
|
+
return terminal.result;
|
|
22579
|
+
}
|
|
22580
|
+
await this.ensureParentPromotion(record);
|
|
22581
|
+
const op = "writeStdin";
|
|
22582
|
+
const admission = await this.deps.beforeProcessMutation?.({
|
|
22583
|
+
op,
|
|
22584
|
+
backend: record.backend,
|
|
22585
|
+
process: record.process
|
|
22586
|
+
});
|
|
22587
|
+
const write = record.backend.session.writeStdin;
|
|
22588
|
+
if (!write) throw new RoutingUnsupportedError(op, record.backend.kind);
|
|
22589
|
+
let result;
|
|
22590
|
+
try {
|
|
22591
|
+
result = await write.call(record.backend.session, args);
|
|
22592
|
+
} catch (error) {
|
|
22593
|
+
if (this.deps.afterProcessMutation) {
|
|
22594
|
+
const pending = {
|
|
22595
|
+
op,
|
|
22596
|
+
backend: record.backend,
|
|
22597
|
+
process: record.process,
|
|
22598
|
+
admission,
|
|
22599
|
+
outcome: "rejected"
|
|
22600
|
+
};
|
|
22601
|
+
try {
|
|
22602
|
+
await this.deps.afterProcessMutation(pending);
|
|
22603
|
+
} catch (settlementError) {
|
|
22604
|
+
record.pendingMutationSettlement = pending;
|
|
22605
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22606
|
+
op,
|
|
22607
|
+
`Retained-process stdin rejected at the provider but lost durable settlement; it was not replayed`,
|
|
22608
|
+
{ cause: settlementError }
|
|
22609
|
+
);
|
|
22610
|
+
}
|
|
22611
|
+
}
|
|
22612
|
+
throw error;
|
|
22613
|
+
}
|
|
22614
|
+
if (this.deps.afterProcessMutation) {
|
|
22615
|
+
const pending = {
|
|
22616
|
+
op,
|
|
22617
|
+
backend: record.backend,
|
|
22618
|
+
process: record.process,
|
|
22619
|
+
admission,
|
|
22620
|
+
outcome: "resolved",
|
|
22621
|
+
result
|
|
22622
|
+
};
|
|
22623
|
+
try {
|
|
22624
|
+
await this.deps.afterProcessMutation(pending);
|
|
22625
|
+
} catch (error) {
|
|
22626
|
+
record.pendingMutationSettlement = pending;
|
|
22627
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22628
|
+
op,
|
|
22629
|
+
`Retained-process stdin returned from the provider but lost durable settlement; tracking remains pinned and it was not replayed`,
|
|
22630
|
+
{ cause: error }
|
|
22631
|
+
);
|
|
22632
|
+
}
|
|
22633
|
+
}
|
|
22634
|
+
const proof = retainedProcessTerminalProof(result, providerSessionId);
|
|
22635
|
+
if (proof) await this.settleRetainedProcess(record, proof, result);
|
|
22636
|
+
return result;
|
|
22637
|
+
}
|
|
22638
|
+
async dispatchProcessControl(args) {
|
|
22639
|
+
const providerSessionId = providerSessionIdFromArgs(args);
|
|
22640
|
+
if (providerSessionId === null) throw new RoutingRetainedProcessNotFoundError(-1);
|
|
22641
|
+
const record = this.retainedProcess(providerSessionId);
|
|
22642
|
+
const priorTerminal = await this.flushPendingProcessMutation(record);
|
|
22643
|
+
if (priorTerminal !== null) return priorTerminal;
|
|
22644
|
+
if (record.pendingTerminal) {
|
|
22645
|
+
const terminal = record.pendingTerminal;
|
|
22646
|
+
await this.settleRetainedProcess(record, terminal.proof, terminal.result);
|
|
22647
|
+
return terminal.result;
|
|
22648
|
+
}
|
|
22649
|
+
await this.ensureParentPromotion(record);
|
|
22650
|
+
const write = record.backend.session.writeStdin;
|
|
22651
|
+
if (!write) throw new RoutingUnsupportedError("writeStdin", record.backend.kind);
|
|
22652
|
+
const result = await write.call(record.backend.session, args);
|
|
22653
|
+
const proof = retainedProcessTerminalProof(result, providerSessionId);
|
|
22654
|
+
if (proof) await this.settleRetainedProcess(record, proof, result);
|
|
22655
|
+
return result;
|
|
21210
22656
|
}
|
|
21211
22657
|
/**
|
|
21212
22658
|
* Dispatch an op to the currently-active backend, retrying on a stale-epoch
|
|
@@ -21219,30 +22665,113 @@ var RoutingSandboxSession = class {
|
|
|
21219
22665
|
* A non-fence error propagates immediately (it is a real op failure, not a swap
|
|
21220
22666
|
* race).
|
|
21221
22667
|
*/
|
|
21222
|
-
async dispatch(op, fn) {
|
|
22668
|
+
async dispatch(op, mutatesWorkspace, fn) {
|
|
21223
22669
|
let attempt = 0;
|
|
21224
22670
|
let lastError;
|
|
21225
22671
|
while (attempt <= this.maxFenceRetries) {
|
|
21226
22672
|
const backend = await this.resolve();
|
|
22673
|
+
const admission = mutatesWorkspace ? await this.deps.beforeMutation?.({ op, backend }) : void 0;
|
|
22674
|
+
let result;
|
|
21227
22675
|
try {
|
|
21228
|
-
|
|
22676
|
+
result = await fn(backend.session);
|
|
21229
22677
|
} catch (error) {
|
|
22678
|
+
if (mutatesWorkspace && this.deps.afterMutation) {
|
|
22679
|
+
try {
|
|
22680
|
+
await this.deps.afterMutation({
|
|
22681
|
+
op,
|
|
22682
|
+
backend,
|
|
22683
|
+
admission,
|
|
22684
|
+
outcome: "rejected"
|
|
22685
|
+
});
|
|
22686
|
+
} catch (settlementError) {
|
|
22687
|
+
this.invalidate(backend);
|
|
22688
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22689
|
+
op,
|
|
22690
|
+
`Mutating sandbox operation "${op}" rejected at the provider but lost its durable physical settlement; its outcome is unknown and it was not replayed`,
|
|
22691
|
+
{ cause: settlementError }
|
|
22692
|
+
);
|
|
22693
|
+
}
|
|
22694
|
+
}
|
|
21230
22695
|
if (!isFenceError(error)) {
|
|
22696
|
+
if (backend.sandboxId === null && this.deps.onDefaultBackendError) {
|
|
22697
|
+
const loss = await this.deps.onDefaultBackendError({
|
|
22698
|
+
op,
|
|
22699
|
+
error,
|
|
22700
|
+
kind: backend.kind,
|
|
22701
|
+
backend
|
|
22702
|
+
});
|
|
22703
|
+
if (loss) {
|
|
22704
|
+
this.invalidate(backend);
|
|
22705
|
+
throw new RoutingBackendRecoveryRequiredError(op, loss.leaseEpoch, loss.recovery);
|
|
22706
|
+
}
|
|
22707
|
+
}
|
|
21231
22708
|
throw error;
|
|
21232
22709
|
}
|
|
22710
|
+
this.invalidate(backend);
|
|
22711
|
+
if (mutatesWorkspace) {
|
|
22712
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22713
|
+
op,
|
|
22714
|
+
`Mutating sandbox operation "${op}" was fenced after provider admission; its outcome is unknown and it was not replayed`,
|
|
22715
|
+
{ cause: error }
|
|
22716
|
+
);
|
|
22717
|
+
}
|
|
21233
22718
|
lastError = error;
|
|
21234
|
-
this.cachedEpoch = void 0;
|
|
21235
|
-
this.cachedSandboxId = void 0;
|
|
21236
|
-
this.cached = void 0;
|
|
21237
|
-
this.deps.onTransition?.({
|
|
21238
|
-
type: "fenced-retry",
|
|
21239
|
-
fromEpoch: backend.sandboxId === null ? 0 : 0,
|
|
21240
|
-
toEpoch: 0,
|
|
21241
|
-
sandboxId: backend.sandboxId,
|
|
21242
|
-
kind: backend.kind
|
|
21243
|
-
});
|
|
21244
22719
|
attempt += 1;
|
|
22720
|
+
continue;
|
|
22721
|
+
}
|
|
22722
|
+
const yieldedSessionId = mutatesWorkspace && (op === "exec" || op === "execCommand") ? providerSessionIdFromResult(result) : null;
|
|
22723
|
+
const retainedProcess = yieldedSessionId === null ? void 0 : { id: crypto.randomUUID(), providerSessionId: yieldedSessionId };
|
|
22724
|
+
const retainedRecord = retainedProcess ? this.registerRetainedProcess(retainedProcess, backend) : null;
|
|
22725
|
+
if (mutatesWorkspace && this.deps.afterMutation) {
|
|
22726
|
+
const settlement = {
|
|
22727
|
+
op,
|
|
22728
|
+
backend,
|
|
22729
|
+
admission,
|
|
22730
|
+
outcome: "resolved",
|
|
22731
|
+
result,
|
|
22732
|
+
...retainedProcess ? { retainedProcess } : {}
|
|
22733
|
+
};
|
|
22734
|
+
try {
|
|
22735
|
+
await this.deps.afterMutation(settlement);
|
|
22736
|
+
if (retainedRecord) retainedRecord.durable = true;
|
|
22737
|
+
} catch (error) {
|
|
22738
|
+
if (retainedRecord) retainedRecord.pendingParentPromotion = settlement;
|
|
22739
|
+
this.invalidate(backend);
|
|
22740
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22741
|
+
op,
|
|
22742
|
+
retainedRecord ? `Mutating sandbox operation "${op}" yielded provider session ${retainedRecord.process.providerSessionId} but lost durable process promotion; exact-backend tracking remains and the operation was not replayed` : `Mutating sandbox operation "${op}" returned from the provider but lost its durable route settlement; its outcome is unknown and it was not replayed`,
|
|
22743
|
+
{ cause: error }
|
|
22744
|
+
);
|
|
22745
|
+
}
|
|
22746
|
+
}
|
|
22747
|
+
if (retainedRecord) return result;
|
|
22748
|
+
let current;
|
|
22749
|
+
try {
|
|
22750
|
+
current = await this.deps.readPointer();
|
|
22751
|
+
} catch (error) {
|
|
22752
|
+
if (mutatesWorkspace) {
|
|
22753
|
+
this.invalidate(backend);
|
|
22754
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22755
|
+
op,
|
|
22756
|
+
`Mutating sandbox operation "${op}" returned from the provider but its route could not be revalidated; its outcome is unknown and it was not replayed`,
|
|
22757
|
+
{ cause: error }
|
|
22758
|
+
);
|
|
22759
|
+
}
|
|
22760
|
+
throw error;
|
|
22761
|
+
}
|
|
22762
|
+
if (current.activeEpoch !== backend.activeEpoch || current.activeSandboxId !== backend.sandboxId) {
|
|
22763
|
+
this.invalidate(backend);
|
|
22764
|
+
if (mutatesWorkspace) {
|
|
22765
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22766
|
+
op,
|
|
22767
|
+
`Mutating sandbox operation "${op}" completed on a superseded route; its output was rejected and it was not replayed`
|
|
22768
|
+
);
|
|
22769
|
+
}
|
|
22770
|
+
lastError = new Error(`sandbox route changed while "${op}" was in flight`);
|
|
22771
|
+
attempt += 1;
|
|
22772
|
+
continue;
|
|
21245
22773
|
}
|
|
22774
|
+
return result;
|
|
21246
22775
|
}
|
|
21247
22776
|
throw lastError ?? new Error(`routing op "${op}" exhausted fence retries`);
|
|
21248
22777
|
}
|
|
@@ -21275,7 +22804,7 @@ var RoutingSandboxSession = class {
|
|
|
21275
22804
|
// dispatches to the active backend at call-time. A missing backend method
|
|
21276
22805
|
// degrades via the natural fallback or RoutingUnsupportedError.
|
|
21277
22806
|
async exec(args) {
|
|
21278
|
-
return this.dispatch("exec", async (s) => {
|
|
22807
|
+
return this.dispatch("exec", true, async (s) => {
|
|
21279
22808
|
if (s.exec) {
|
|
21280
22809
|
return s.exec(args);
|
|
21281
22810
|
}
|
|
@@ -21287,7 +22816,7 @@ var RoutingSandboxSession = class {
|
|
|
21287
22816
|
}
|
|
21288
22817
|
async execCommand(args) {
|
|
21289
22818
|
try {
|
|
21290
|
-
return await this.dispatch("execCommand", async (s) => {
|
|
22819
|
+
return await this.dispatch("execCommand", true, async (s) => {
|
|
21291
22820
|
if (s.execCommand) {
|
|
21292
22821
|
return s.execCommand(args);
|
|
21293
22822
|
}
|
|
@@ -21302,21 +22831,72 @@ var RoutingSandboxSession = class {
|
|
|
21302
22831
|
}
|
|
21303
22832
|
}
|
|
21304
22833
|
async writeStdin(args) {
|
|
21305
|
-
|
|
22834
|
+
const providerSessionId = providerSessionIdFromArgs(args);
|
|
22835
|
+
if (providerSessionId !== null && this.retainedProcesses.has(providerSessionId)) {
|
|
22836
|
+
return await this.dispatchProcessMutation(args);
|
|
22837
|
+
}
|
|
22838
|
+
return this.dispatch("writeStdin", true, async (s) => {
|
|
21306
22839
|
if (!s.writeStdin) {
|
|
21307
22840
|
throw new RoutingUnsupportedError("writeStdin", this.cached?.kind ?? "unknown");
|
|
21308
22841
|
}
|
|
21309
22842
|
return s.writeStdin(args);
|
|
21310
22843
|
});
|
|
21311
22844
|
}
|
|
22845
|
+
/** Whether a positive provider session locator is still pinned to the exact
|
|
22846
|
+
* backend that yielded it. This synchronous probe is used only to select the
|
|
22847
|
+
* process-aware routing surface; it is not itself durable authority. */
|
|
22848
|
+
hasRetainedProcess(providerSessionId) {
|
|
22849
|
+
return positiveProviderSessionId(providerSessionId) !== null && this.retainedProcesses.has(providerSessionId);
|
|
22850
|
+
}
|
|
22851
|
+
/** Return only OpenGeni's durable UUID + provider locator. Backend/session
|
|
22852
|
+
* objects remain private so a caller cannot forge route authority from this
|
|
22853
|
+
* diagnostic handoff. */
|
|
22854
|
+
retainedProcessIdentity(providerSessionId) {
|
|
22855
|
+
const record = this.retainedProcesses.get(providerSessionId);
|
|
22856
|
+
return record ? { ...record.process } : null;
|
|
22857
|
+
}
|
|
22858
|
+
/** Model/user-visible stdin is a distinct workspace mutation admission under
|
|
22859
|
+
* the durable retained-process holder. It never re-reads the active pointer. */
|
|
22860
|
+
async writeStdinForProcessMutation(args) {
|
|
22861
|
+
return await this.dispatchProcessMutation(args);
|
|
22862
|
+
}
|
|
22863
|
+
/** Cancellation and drain polling are control operations. They stay pinned to
|
|
22864
|
+
* the process backend and may prove terminal state, but never advance the
|
|
22865
|
+
* workspace generation. */
|
|
22866
|
+
async writeStdinForProcessControl(args) {
|
|
22867
|
+
return await this.dispatchProcessControl(args);
|
|
22868
|
+
}
|
|
22869
|
+
/** Run a PID/PGID marker or signal helper on the retained process's exact
|
|
22870
|
+
* backend. The helper is control-plane work and therefore has no workspace
|
|
22871
|
+
* mutation admission of its own. */
|
|
22872
|
+
async execCommandForProcessControl(providerSessionId, args) {
|
|
22873
|
+
const exactProviderSessionId = positiveProviderSessionId(providerSessionId);
|
|
22874
|
+
if (exactProviderSessionId === null) throw new RoutingRetainedProcessNotFoundError(-1);
|
|
22875
|
+
const record = this.retainedProcess(exactProviderSessionId);
|
|
22876
|
+
const priorTerminal = await this.flushPendingProcessMutation(record);
|
|
22877
|
+
if (priorTerminal !== null) return priorTerminal;
|
|
22878
|
+
if (record.pendingTerminal) {
|
|
22879
|
+
const terminal = record.pendingTerminal;
|
|
22880
|
+
await this.settleRetainedProcess(record, terminal.proof, terminal.result);
|
|
22881
|
+
return terminal.result;
|
|
22882
|
+
}
|
|
22883
|
+
await this.ensureParentPromotion(record);
|
|
22884
|
+
if (record.backend.session.execCommand) {
|
|
22885
|
+
return await record.backend.session.execCommand(args);
|
|
22886
|
+
}
|
|
22887
|
+
if (record.backend.session.exec) {
|
|
22888
|
+
return formatExecResult(await record.backend.session.exec(args));
|
|
22889
|
+
}
|
|
22890
|
+
throw new RoutingUnsupportedError("execCommand", record.backend.kind);
|
|
22891
|
+
}
|
|
21312
22892
|
async cancelExecCommand(opId) {
|
|
21313
|
-
return await this.dispatch("cancelExecCommand", async (session) => {
|
|
22893
|
+
return await this.dispatch("cancelExecCommand", false, async (session) => {
|
|
21314
22894
|
if (!session.cancelExecCommand) return false;
|
|
21315
22895
|
return await session.cancelExecCommand(opId);
|
|
21316
22896
|
});
|
|
21317
22897
|
}
|
|
21318
22898
|
async readFile(args) {
|
|
21319
|
-
return this.dispatch("readFile", async (s) => {
|
|
22899
|
+
return this.dispatch("readFile", false, async (s) => {
|
|
21320
22900
|
if (!s.readFile) {
|
|
21321
22901
|
throw new RoutingUnsupportedError("readFile", this.cached?.kind ?? "unknown");
|
|
21322
22902
|
}
|
|
@@ -21324,7 +22904,7 @@ var RoutingSandboxSession = class {
|
|
|
21324
22904
|
});
|
|
21325
22905
|
}
|
|
21326
22906
|
async writeFile(args) {
|
|
21327
|
-
return this.dispatch("writeFile", async (s) => {
|
|
22907
|
+
return this.dispatch("writeFile", true, async (s) => {
|
|
21328
22908
|
if (!s.writeFile) {
|
|
21329
22909
|
throw new RoutingUnsupportedError("writeFile", this.cached?.kind ?? "unknown");
|
|
21330
22910
|
}
|
|
@@ -21332,7 +22912,7 @@ var RoutingSandboxSession = class {
|
|
|
21332
22912
|
});
|
|
21333
22913
|
}
|
|
21334
22914
|
async listDir(args) {
|
|
21335
|
-
return this.dispatch("listDir", async (s) => {
|
|
22915
|
+
return this.dispatch("listDir", false, async (s) => {
|
|
21336
22916
|
if (!s.listDir) {
|
|
21337
22917
|
throw new RoutingUnsupportedError("listDir", this.cached?.kind ?? "unknown");
|
|
21338
22918
|
}
|
|
@@ -21340,7 +22920,7 @@ var RoutingSandboxSession = class {
|
|
|
21340
22920
|
});
|
|
21341
22921
|
}
|
|
21342
22922
|
async pathExists(path, runAs) {
|
|
21343
|
-
return this.dispatch("pathExists", async (s) => {
|
|
22923
|
+
return this.dispatch("pathExists", false, async (s) => {
|
|
21344
22924
|
if (!s.pathExists) {
|
|
21345
22925
|
throw new RoutingUnsupportedError("pathExists", this.cached?.kind ?? "unknown");
|
|
21346
22926
|
}
|
|
@@ -21348,7 +22928,7 @@ var RoutingSandboxSession = class {
|
|
|
21348
22928
|
});
|
|
21349
22929
|
}
|
|
21350
22930
|
async viewImage(args) {
|
|
21351
|
-
return this.dispatch("viewImage", async (s) => {
|
|
22931
|
+
return this.dispatch("viewImage", false, async (s) => {
|
|
21352
22932
|
if (!s.viewImage) {
|
|
21353
22933
|
throw new RoutingUnsupportedError("viewImage", this.cached?.kind ?? "unknown");
|
|
21354
22934
|
}
|
|
@@ -21356,7 +22936,7 @@ var RoutingSandboxSession = class {
|
|
|
21356
22936
|
});
|
|
21357
22937
|
}
|
|
21358
22938
|
async materializeEntry(args) {
|
|
21359
|
-
return this.dispatch("materializeEntry", async (s) => {
|
|
22939
|
+
return this.dispatch("materializeEntry", true, async (s) => {
|
|
21360
22940
|
if (!s.materializeEntry) {
|
|
21361
22941
|
throw new RoutingUnsupportedError("materializeEntry", this.cached?.kind ?? "unknown");
|
|
21362
22942
|
}
|
|
@@ -21382,13 +22962,12 @@ var RoutingSandboxSession = class {
|
|
|
21382
22962
|
* direct delegate returns undefined and every lazy turn would die at bind. Return a
|
|
21383
22963
|
* LAZY EDITOR PROXY instead: a non-null editor whose async ops resolve the active
|
|
21384
22964
|
* backend (establishing the box on first use, via `dispatch`) and delegate to its
|
|
21385
|
-
* real editor — mirroring how this proxy defers exec/readFile.
|
|
22965
|
+
* real editor — mirroring how this proxy defers exec/readFile. Even when an
|
|
22966
|
+
* eager editor already exists, returning it directly would bypass per-edit
|
|
22967
|
+
* route resolution and mutation-generation admission, so every editor is
|
|
22968
|
+
* represented by this dispatching proxy. */
|
|
21386
22969
|
createEditor(runAs) {
|
|
21387
|
-
const
|
|
21388
|
-
if (eager) {
|
|
21389
|
-
return eager;
|
|
21390
|
-
}
|
|
21391
|
-
const op = (name) => (operation, context) => this.dispatch(`editor.${name}`, async (s) => {
|
|
22970
|
+
const op = (name) => (operation, context) => this.dispatch(`editor.${name}`, true, async (s) => {
|
|
21392
22971
|
const editor = s.createEditor?.(runAs);
|
|
21393
22972
|
if (!editor?.[name]) {
|
|
21394
22973
|
throw new RoutingUnsupportedError(`editor.${name}`, this.cached?.kind ?? "unknown");
|
|
@@ -21402,7 +22981,7 @@ var RoutingSandboxSession = class {
|
|
|
21402
22981
|
};
|
|
21403
22982
|
}
|
|
21404
22983
|
async resolveExposedPort(port) {
|
|
21405
|
-
return this.dispatch("resolveExposedPort", async (s) => {
|
|
22984
|
+
return this.dispatch("resolveExposedPort", false, async (s) => {
|
|
21406
22985
|
if (!s.resolveExposedPort) {
|
|
21407
22986
|
throw new RoutingUnsupportedError("resolveExposedPort", this.cached?.kind ?? "unknown");
|
|
21408
22987
|
}
|
|
@@ -21412,7 +22991,7 @@ var RoutingSandboxSession = class {
|
|
|
21412
22991
|
/** Serialize the active backend's session state. Used by the resume-by-id seam
|
|
21413
22992
|
* to fold the live box onto the lease. Dispatches to the active backend. */
|
|
21414
22993
|
async serializeSessionState() {
|
|
21415
|
-
return this.dispatch("serializeSessionState", async (s) => {
|
|
22994
|
+
return this.dispatch("serializeSessionState", false, async (s) => {
|
|
21416
22995
|
if (!s.serializeSessionState) {
|
|
21417
22996
|
return void 0;
|
|
21418
22997
|
}
|
|
@@ -21464,6 +23043,9 @@ function makeActiveBackendResolver(deps) {
|
|
|
21464
23043
|
"the active sandbox was detached to the session default (home), but this turn started pinned to a machine and established no home box; the detach is accepted and takes effect on the next turn \u2014 this turn has no active home box"
|
|
21465
23044
|
);
|
|
21466
23045
|
}
|
|
23046
|
+
if (deps.resolveDefaultBackend) {
|
|
23047
|
+
return await deps.resolveDefaultBackend(pointer);
|
|
23048
|
+
}
|
|
21467
23049
|
return { session: deps.defaultBackend, sandboxId: null, kind: deps.defaultKind };
|
|
21468
23050
|
}
|
|
21469
23051
|
if (deps.pinnedSelfhosted && pointer.activeSandboxId === deps.pinnedSelfhosted.sandboxId && pointer.activeEpoch === deps.pinnedSelfhosted.epoch) {
|
|
@@ -21646,20 +23228,6 @@ function readWorkspaceArchiveFromEnvelopeSessionState(sessionState) {
|
|
|
21646
23228
|
return void 0;
|
|
21647
23229
|
}
|
|
21648
23230
|
}
|
|
21649
|
-
function readWorkspaceArchivePairFromEnvelopeSessionState(sessionState) {
|
|
21650
|
-
if (!sessionState || typeof sessionState !== "object") {
|
|
21651
|
-
return {};
|
|
21652
|
-
}
|
|
21653
|
-
const state = sessionState;
|
|
21654
|
-
const current = readWorkspaceArchiveFromEnvelopeSessionState(sessionState);
|
|
21655
|
-
const previous = readWorkspaceArchiveFromEnvelopeSessionState({
|
|
21656
|
-
workspaceArchive: state.workspaceArchivePrev
|
|
21657
|
-
});
|
|
21658
|
-
return {
|
|
21659
|
-
...current ? { current } : {},
|
|
21660
|
-
...previous ? { previous } : {}
|
|
21661
|
-
};
|
|
21662
|
-
}
|
|
21663
23231
|
var MODAL_SNAPSHOT_REF_PREFIXES = [
|
|
21664
23232
|
"MODAL_SANDBOX_FS_SNAPSHOT_V1\n",
|
|
21665
23233
|
"MODAL_SANDBOX_DIR_SNAPSHOT_V1\n"
|
|
@@ -21730,39 +23298,73 @@ async function deserializeSandboxSessionStateEnvelope(client, envelope) {
|
|
|
21730
23298
|
...state.exposedPorts ? { exposedPorts: structuredClone(state.exposedPorts) } : {}
|
|
21731
23299
|
});
|
|
21732
23300
|
}
|
|
21733
|
-
|
|
21734
|
-
|
|
21735
|
-
|
|
23301
|
+
var SandboxExecReadinessError = class extends Error {
|
|
23302
|
+
constructor(backend, code, timeoutMs, exitCode = null) {
|
|
23303
|
+
super(
|
|
23304
|
+
code === "exec_probe_timeout" ? `sandbox creation timed out waiting for ${backend} command readiness after ${timeoutMs}ms` : code === "exec_probe_unavailable" ? `${backend} sandbox session does not expose an exec readiness probe` : exitCode === null ? `${backend} sandbox exec readiness probe did not return a command exit code` : `${backend} sandbox exec readiness probe failed with exit code ${exitCode}`
|
|
23305
|
+
);
|
|
23306
|
+
this.backend = backend;
|
|
23307
|
+
this.code = code;
|
|
23308
|
+
this.timeoutMs = timeoutMs;
|
|
23309
|
+
this.exitCode = exitCode;
|
|
21736
23310
|
}
|
|
21737
|
-
|
|
21738
|
-
|
|
23311
|
+
name = "SandboxExecReadinessError";
|
|
23312
|
+
};
|
|
23313
|
+
function sandboxExecProbeExitCode(result) {
|
|
23314
|
+
if (typeof result === "string") {
|
|
23315
|
+
const match = result.match(/Process exited with code (-?\d+)/);
|
|
23316
|
+
return match ? Number(match[1]) : null;
|
|
21739
23317
|
}
|
|
21740
|
-
|
|
21741
|
-
|
|
21742
|
-
|
|
23318
|
+
if (!result || typeof result !== "object") return null;
|
|
23319
|
+
const candidate = result;
|
|
23320
|
+
for (const value of [candidate.exitCode, candidate.exit_code, candidate.code, candidate.status]) {
|
|
23321
|
+
if (typeof value === "number") return value;
|
|
21743
23322
|
}
|
|
21744
|
-
|
|
21745
|
-
|
|
21746
|
-
|
|
21747
|
-
|
|
21748
|
-
|
|
21749
|
-
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
21763
|
-
|
|
23323
|
+
return null;
|
|
23324
|
+
}
|
|
23325
|
+
function sandboxExecProbeStillRunning(result) {
|
|
23326
|
+
if (typeof result === "string") return /Process running with session ID \d+/u.test(result);
|
|
23327
|
+
if (!result || typeof result !== "object") return false;
|
|
23328
|
+
const candidate = result;
|
|
23329
|
+
return typeof candidate.sessionId === "number" || typeof candidate.session_id === "number";
|
|
23330
|
+
}
|
|
23331
|
+
async function verifySandboxExecReadiness(established, timeoutMs = 15e3) {
|
|
23332
|
+
if (established.backendId !== "modal") return;
|
|
23333
|
+
const session = established.session;
|
|
23334
|
+
const run2 = session.exec ?? session.execCommand;
|
|
23335
|
+
if (!run2) {
|
|
23336
|
+
throw new SandboxExecReadinessError(established.backendId, "exec_probe_unavailable", timeoutMs);
|
|
23337
|
+
}
|
|
23338
|
+
let timer;
|
|
23339
|
+
try {
|
|
23340
|
+
const result = await Promise.race([
|
|
23341
|
+
run2.call(session, {
|
|
23342
|
+
cmd: "true",
|
|
23343
|
+
yieldTimeMs: 1e3,
|
|
23344
|
+
maxOutputTokens: 1e3
|
|
23345
|
+
}),
|
|
23346
|
+
new Promise((_, reject) => {
|
|
23347
|
+
timer = setTimeout(
|
|
23348
|
+
() => reject(
|
|
23349
|
+
new SandboxExecReadinessError(established.backendId, "exec_probe_timeout", timeoutMs)
|
|
23350
|
+
),
|
|
23351
|
+
timeoutMs
|
|
23352
|
+
);
|
|
23353
|
+
if (timer && "unref" in timer && typeof timer.unref === "function") timer.unref();
|
|
23354
|
+
})
|
|
23355
|
+
]);
|
|
23356
|
+
const exitCode = sandboxExecProbeExitCode(result);
|
|
23357
|
+
if (sandboxExecProbeStillRunning(result) || exitCode !== 0) {
|
|
23358
|
+
throw new SandboxExecReadinessError(
|
|
23359
|
+
established.backendId,
|
|
23360
|
+
"exec_probe_failed",
|
|
23361
|
+
timeoutMs,
|
|
23362
|
+
exitCode
|
|
23363
|
+
);
|
|
23364
|
+
}
|
|
23365
|
+
} finally {
|
|
23366
|
+
if (timer) clearTimeout(timer);
|
|
21764
23367
|
}
|
|
21765
|
-
return goneMarkers.some((marker) => haystack.includes(marker));
|
|
21766
23368
|
}
|
|
21767
23369
|
function readInstanceId(session) {
|
|
21768
23370
|
const state = session.state ?? {};
|
|
@@ -21800,9 +23402,14 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
|
|
|
21800
23402
|
}
|
|
21801
23403
|
const createManifest = { environment };
|
|
21802
23404
|
const envelopeSessionState = envelope && typeof envelope === "object" ? envelope.sessionState : void 0;
|
|
21803
|
-
const
|
|
21804
|
-
const
|
|
21805
|
-
const
|
|
23405
|
+
const archiveState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState : void 0;
|
|
23406
|
+
const workspaceArchiveBase64 = archiveState?.workspaceArchive;
|
|
23407
|
+
const workspaceArchiveMetadata = archiveState?.workspaceArchiveMeta;
|
|
23408
|
+
const coldRestore = async (resumeFallbackState) => {
|
|
23409
|
+
const workspaceArchive = readVerifiedWorkspaceArchive(
|
|
23410
|
+
workspaceArchiveBase64,
|
|
23411
|
+
workspaceArchiveMetadata
|
|
23412
|
+
);
|
|
21806
23413
|
const createStarted = Date.now();
|
|
21807
23414
|
let restored;
|
|
21808
23415
|
try {
|
|
@@ -21829,56 +23436,68 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
|
|
|
21829
23436
|
}
|
|
21830
23437
|
}
|
|
21831
23438
|
let hydrationApplied = false;
|
|
21832
|
-
if (
|
|
23439
|
+
if (workspaceArchive) {
|
|
21833
23440
|
const hydrate = restored.hydrateWorkspace;
|
|
21834
|
-
if (typeof hydrate
|
|
21835
|
-
|
|
21836
|
-
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
23441
|
+
if (typeof hydrate !== "function") {
|
|
23442
|
+
await terminateCreatedSandbox(client, restored, restoredState);
|
|
23443
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
23444
|
+
"archive_hydration_failed",
|
|
23445
|
+
`sandbox backend ${client.backendId} cannot hydrate selected archive revision ${workspaceArchive.descriptor.revision}`
|
|
23446
|
+
);
|
|
23447
|
+
}
|
|
23448
|
+
try {
|
|
23449
|
+
await hydrate.call(restored, workspaceArchive.bytes);
|
|
23450
|
+
} catch (error) {
|
|
23451
|
+
await terminateCreatedSandbox(client, restored, restored.state);
|
|
23452
|
+
if (error instanceof WorkspaceArchiveIntegrityError) throw error;
|
|
23453
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
23454
|
+
"archive_hydration_failed",
|
|
23455
|
+
`failed to hydrate selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
|
|
23456
|
+
{ retryable: true }
|
|
23457
|
+
);
|
|
23458
|
+
}
|
|
23459
|
+
const hydratedState = restored.state;
|
|
23460
|
+
const hydratedInstanceId = readInstanceId(restored);
|
|
23461
|
+
if (hydratedInstanceId && hydratedInstanceId !== established.instanceId) {
|
|
23462
|
+
established = {
|
|
23463
|
+
client,
|
|
23464
|
+
session: restored,
|
|
23465
|
+
sessionState: hydratedState ?? resumeFallbackState,
|
|
23466
|
+
instanceId: hydratedInstanceId,
|
|
23467
|
+
backendId: client.backendId
|
|
23468
|
+
};
|
|
23469
|
+
if (opts.onSandboxCreated) {
|
|
21843
23470
|
try {
|
|
21844
|
-
await
|
|
21845
|
-
|
|
21846
|
-
|
|
21847
|
-
|
|
21848
|
-
hydrated = true;
|
|
21849
|
-
break;
|
|
21850
|
-
} catch (hydrateError) {
|
|
21851
|
-
console.warn(
|
|
21852
|
-
`[sandbox] cold-restore failed to hydrate workspace from ${candidate.label} archive${candidate.label === "current" && workspaceArchives.previous ? "; trying previous archive" : ""}`,
|
|
21853
|
-
hydrateError
|
|
21854
|
-
);
|
|
23471
|
+
await opts.onSandboxCreated(established);
|
|
23472
|
+
} catch (createCallbackError) {
|
|
23473
|
+
await terminateCreatedSandbox(client, restored, hydratedState);
|
|
23474
|
+
throw createCallbackError;
|
|
21855
23475
|
}
|
|
21856
23476
|
}
|
|
21857
|
-
|
|
21858
|
-
|
|
21859
|
-
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
if (hydratedInstanceId && hydratedInstanceId !== established.instanceId) {
|
|
21865
|
-
established = {
|
|
21866
|
-
client,
|
|
21867
|
-
session: restored,
|
|
21868
|
-
sessionState: hydratedState ?? resumeFallbackState,
|
|
21869
|
-
instanceId: hydratedInstanceId,
|
|
21870
|
-
backendId: client.backendId
|
|
21871
|
-
};
|
|
21872
|
-
if (opts.onSandboxCreated) {
|
|
21873
|
-
try {
|
|
21874
|
-
await opts.onSandboxCreated(established);
|
|
21875
|
-
} catch (createCallbackError) {
|
|
21876
|
-
await terminateCreatedSandbox(client, restored, hydratedState);
|
|
21877
|
-
throw createCallbackError;
|
|
21878
|
-
}
|
|
21879
|
-
}
|
|
23477
|
+
}
|
|
23478
|
+
if (opts.onWorkspaceRestoreVerifying) {
|
|
23479
|
+
try {
|
|
23480
|
+
await opts.onWorkspaceRestoreVerifying(workspaceArchive.descriptor);
|
|
23481
|
+
} catch (error) {
|
|
23482
|
+
await terminateCreatedSandbox(client, restored, restored.state);
|
|
23483
|
+
throw error;
|
|
21880
23484
|
}
|
|
21881
23485
|
}
|
|
23486
|
+
try {
|
|
23487
|
+
await verifyRestoredWorkspace(restored, workspaceArchive.descriptor);
|
|
23488
|
+
} catch (error) {
|
|
23489
|
+
await terminateCreatedSandbox(client, restored, restored.state);
|
|
23490
|
+
if (error instanceof WorkspaceArchiveIntegrityError) throw error;
|
|
23491
|
+
throw new WorkspaceArchiveIntegrityError(
|
|
23492
|
+
"workspace_fingerprint_unavailable",
|
|
23493
|
+
`failed to verify selected workspace archive revision ${workspaceArchive.descriptor.revision}`,
|
|
23494
|
+
{ retryable: true }
|
|
23495
|
+
);
|
|
23496
|
+
}
|
|
23497
|
+
hydrationApplied = true;
|
|
23498
|
+
console.info(
|
|
23499
|
+
`[sandbox] cold-restore verified workspace archive revision ${workspaceArchive.descriptor.revision}`
|
|
23500
|
+
);
|
|
21882
23501
|
}
|
|
21883
23502
|
restoredState = restored.state;
|
|
21884
23503
|
return {
|
|
@@ -21887,7 +23506,8 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
|
|
|
21887
23506
|
sessionState: restoredState ?? resumeFallbackState,
|
|
21888
23507
|
instanceId: readInstanceId(restored),
|
|
21889
23508
|
backendId: client.backendId,
|
|
21890
|
-
origin: hydrationApplied ? "restored" : "created"
|
|
23509
|
+
origin: hydrationApplied ? "restored" : "created",
|
|
23510
|
+
...workspaceArchive ? { restoredArchive: workspaceArchive.descriptor } : {}
|
|
21891
23511
|
};
|
|
21892
23512
|
};
|
|
21893
23513
|
const envelopeProviderState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState.providerState : void 0;
|
|
@@ -21973,6 +23593,79 @@ async function serializeEstablishedSandboxEnvelope(established) {
|
|
|
21973
23593
|
return null;
|
|
21974
23594
|
}
|
|
21975
23595
|
}
|
|
23596
|
+
var PROVIDER_IDENTITY_FIELDS = [
|
|
23597
|
+
"sandboxId",
|
|
23598
|
+
"instanceId",
|
|
23599
|
+
"id",
|
|
23600
|
+
"hostId",
|
|
23601
|
+
"containerId",
|
|
23602
|
+
"workspaceRootPath",
|
|
23603
|
+
"agentId"
|
|
23604
|
+
];
|
|
23605
|
+
function hasPersistableSandboxProviderIdentity(envelope) {
|
|
23606
|
+
const sessionState = envelope?.sessionState && typeof envelope.sessionState === "object" ? envelope.sessionState : null;
|
|
23607
|
+
const providerState = sessionState?.providerState && typeof sessionState.providerState === "object" ? sessionState.providerState : null;
|
|
23608
|
+
return Boolean(
|
|
23609
|
+
providerState && PROVIDER_IDENTITY_FIELDS.some(
|
|
23610
|
+
(field) => typeof providerState[field] === "string" && providerState[field].length > 0
|
|
23611
|
+
)
|
|
23612
|
+
);
|
|
23613
|
+
}
|
|
23614
|
+
var SandboxReplacementProviderStateError = class extends Error {
|
|
23615
|
+
constructor(backend) {
|
|
23616
|
+
super(
|
|
23617
|
+
`sandbox backend "${backend}" replacement provider state could not be serialized; refusing to publish an attachable lease`
|
|
23618
|
+
);
|
|
23619
|
+
this.backend = backend;
|
|
23620
|
+
this.name = "SandboxReplacementProviderStateError";
|
|
23621
|
+
}
|
|
23622
|
+
code = "replacement_provider_state_unpersistable";
|
|
23623
|
+
};
|
|
23624
|
+
function requirePersistableReplacementSandboxEnvelope(envelope, backend) {
|
|
23625
|
+
if (!hasPersistableSandboxProviderIdentity(envelope)) {
|
|
23626
|
+
throw new SandboxReplacementProviderStateError(backend);
|
|
23627
|
+
}
|
|
23628
|
+
return envelope;
|
|
23629
|
+
}
|
|
23630
|
+
var DURABLE_WORKSPACE_ARCHIVE_FIELDS = [
|
|
23631
|
+
"workspaceArchive",
|
|
23632
|
+
"workspaceArchiveMeta",
|
|
23633
|
+
"workspaceArchivePrev",
|
|
23634
|
+
"workspaceArchivePrevMeta",
|
|
23635
|
+
"workspaceArchiveAt"
|
|
23636
|
+
];
|
|
23637
|
+
function durableWorkspaceArchiveFields(envelope) {
|
|
23638
|
+
const sessionState = envelope?.sessionState && typeof envelope.sessionState === "object" ? envelope.sessionState : null;
|
|
23639
|
+
if (!sessionState || typeof sessionState.workspaceArchive !== "string" || sessionState.workspaceArchive.length === 0) {
|
|
23640
|
+
return null;
|
|
23641
|
+
}
|
|
23642
|
+
const fields = {};
|
|
23643
|
+
for (const key of DURABLE_WORKSPACE_ARCHIVE_FIELDS) {
|
|
23644
|
+
if (sessionState[key] !== void 0 && sessionState[key] !== null) {
|
|
23645
|
+
fields[key] = sessionState[key];
|
|
23646
|
+
}
|
|
23647
|
+
}
|
|
23648
|
+
return fields;
|
|
23649
|
+
}
|
|
23650
|
+
async function serializeReplacementSandboxEnvelope(established, archiveSource) {
|
|
23651
|
+
const serialized = await serializeEstablishedSandboxEnvelope(established);
|
|
23652
|
+
const archiveFields = durableWorkspaceArchiveFields(archiveSource);
|
|
23653
|
+
if (!serialized && !archiveFields) {
|
|
23654
|
+
return null;
|
|
23655
|
+
}
|
|
23656
|
+
const serializedSessionState = serialized?.sessionState && typeof serialized.sessionState === "object" ? serialized.sessionState : {};
|
|
23657
|
+
return {
|
|
23658
|
+
...serialized ?? {},
|
|
23659
|
+
// Never inherit a historical backend marker when the replacement could not
|
|
23660
|
+
// serialize. The separately-persisted resume_backend_id and this envelope
|
|
23661
|
+
// must both describe the replacement.
|
|
23662
|
+
backendId: established.backendId,
|
|
23663
|
+
sessionState: {
|
|
23664
|
+
...serializedSessionState,
|
|
23665
|
+
...archiveFields ?? {}
|
|
23666
|
+
}
|
|
23667
|
+
};
|
|
23668
|
+
}
|
|
21976
23669
|
|
|
21977
23670
|
export {
|
|
21978
23671
|
localDirLazySkillSource,
|
|
@@ -22040,6 +23733,17 @@ export {
|
|
|
22040
23733
|
isSelfhostedProviderNotFoundError,
|
|
22041
23734
|
PROVIDER_REGISTRY,
|
|
22042
23735
|
assertProviderRegistryInvariants,
|
|
23736
|
+
classifyProviderSandboxFailure,
|
|
23737
|
+
isProviderSandboxNotFoundError2 as isProviderSandboxNotFoundError,
|
|
23738
|
+
isProviderSandboxGoneDuringRoutedOperation,
|
|
23739
|
+
isProviderSandboxTransientError,
|
|
23740
|
+
WORKSPACE_ARCHIVE_DESCRIPTOR_VERSION,
|
|
23741
|
+
WorkspaceArchiveIntegrityError,
|
|
23742
|
+
parseWorkspaceArchiveDescriptor,
|
|
23743
|
+
readVerifiedWorkspaceArchive,
|
|
23744
|
+
fingerprintSandboxWorkspace,
|
|
23745
|
+
captureVerifiedWorkspaceArchive,
|
|
23746
|
+
verifyRestoredWorkspace,
|
|
22043
23747
|
selectBackend,
|
|
22044
23748
|
backendSupportsOs,
|
|
22045
23749
|
desktopCapableBackend,
|
|
@@ -22050,6 +23754,7 @@ export {
|
|
|
22050
23754
|
StreamTokenPayload2 as StreamTokenPayload,
|
|
22051
23755
|
parseExecBannerSessionId,
|
|
22052
23756
|
parseExecBannerExitCode,
|
|
23757
|
+
isExecSessionLostBanner,
|
|
22053
23758
|
STREAM_PORT,
|
|
22054
23759
|
DISPLAY_STACK_TIMEOUT_MS,
|
|
22055
23760
|
DEFAULT_DESKTOP_GEOMETRY,
|
|
@@ -22094,6 +23799,9 @@ export {
|
|
|
22094
23799
|
uploadRecordingArtifact,
|
|
22095
23800
|
deleteRecordingArtifacts,
|
|
22096
23801
|
recordingStorageKey,
|
|
23802
|
+
TurnSandboxCommandCancelledError,
|
|
23803
|
+
createTurnToolCancellationController,
|
|
23804
|
+
wrapCapabilityToolsForTurnCancellation,
|
|
22097
23805
|
ChannelAValidationError,
|
|
22098
23806
|
ChannelAUnavailableError,
|
|
22099
23807
|
ChannelAConflictError,
|
|
@@ -22102,7 +23810,6 @@ export {
|
|
|
22102
23810
|
REPOSITORY_DISCOVERY_LIMIT,
|
|
22103
23811
|
SandboxChannelAService,
|
|
22104
23812
|
stripExecBanner2 as stripExecBanner,
|
|
22105
|
-
isExecSessionLostBanner,
|
|
22106
23813
|
assertSafeRelPath,
|
|
22107
23814
|
parsePorcelainV2,
|
|
22108
23815
|
parseNumstatZ,
|
|
@@ -22112,6 +23819,9 @@ export {
|
|
|
22112
23819
|
negotiateSelfhostedCapabilities,
|
|
22113
23820
|
MockAgentResponder,
|
|
22114
23821
|
RoutingUnsupportedError,
|
|
23822
|
+
RoutingBackendRecoveryRequiredError,
|
|
23823
|
+
RoutingMutationOutcomeUnknownError,
|
|
23824
|
+
RoutingRetainedProcessNotFoundError,
|
|
22115
23825
|
RoutingSandboxSession,
|
|
22116
23826
|
ActiveBackendUnresolvableError,
|
|
22117
23827
|
swapTargetEstablishability,
|
|
@@ -22124,10 +23834,15 @@ export {
|
|
|
22124
23834
|
decodeModalSnapshotId,
|
|
22125
23835
|
deletePriorPersistedSnapshot,
|
|
22126
23836
|
deserializeSandboxSessionStateEnvelope,
|
|
22127
|
-
|
|
23837
|
+
SandboxExecReadinessError,
|
|
23838
|
+
verifySandboxExecReadiness,
|
|
22128
23839
|
establishSandboxSessionFromEnvelope,
|
|
22129
23840
|
serializeEstablishedSandboxEnvelope,
|
|
23841
|
+
hasPersistableSandboxProviderIdentity,
|
|
23842
|
+
SandboxReplacementProviderStateError,
|
|
23843
|
+
requirePersistableReplacementSandboxEnvelope,
|
|
23844
|
+
serializeReplacementSandboxEnvelope,
|
|
22130
23845
|
collectSandboxEnvironment2 as collectSandboxEnvironment,
|
|
22131
23846
|
parseExposedPorts2 as parseExposedPorts
|
|
22132
23847
|
};
|
|
22133
|
-
//# sourceMappingURL=chunk-
|
|
23848
|
+
//# sourceMappingURL=chunk-JGUQSAG5.js.map
|