@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.
@@ -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 createHash4 } from "crypto";
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 = createHash4("sha256").update(assertToolspaceSessionId(sessionId)).digest("hex");
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 recordingStorageKey(workspaceId, sessionId, recordingId, codec) {
19455
- return `recordings/${workspaceId}/${sessionId}/${recordingId}.${extForCodec(codec)}`;
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, return buffered stdout/stderr + exit code inline. The
20277
- * long-running tail (when the process hasn't exited within timeoutMs) keeps
20278
- * running in-box; if emitStream is set the buffered output is also published as
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 r = await this.run({
20282
- cmd: req.command,
20283
- workdir: this.terminalWorkdir(req.cwd),
20284
- yieldTimeMs: req.timeoutMs
20285
- });
20286
- const running = r.exitCode === null && typeof r.sessionId === "number";
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
- if (!this.session.writeStdin) {
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 this.session.writeStdin({
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
- if (!this.session.writeStdin) return;
20353
- await this.session.writeStdin({
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
- /** Close an open PTY: write exit/EOF. The caller marks the row closed + emits
20361
- * terminal.pty.exited. */
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
- if (execSessionId !== null && this.session.writeStdin) {
20364
- try {
20365
- await this.session.writeStdin({ sessionId: execSessionId, chars: "", yieldTimeMs: 50 });
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 = resolved;
21201
- this.lastResolved = resolved;
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: resolved.sandboxId,
21207
- kind: resolved.kind
22418
+ sandboxId: routed.sandboxId,
22419
+ kind: routed.kind
21208
22420
  });
21209
- return resolved;
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
- return await fn(backend.session);
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
- return this.dispatch("writeStdin", async (s) => {
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 eager = (this.lastResolved ?? this.deps.defaultResolved)?.session.createEditor?.(runAs);
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
- function isProviderSandboxNotFoundError2(backendId, error) {
21734
- if (backendId === "selfhosted") {
21735
- return isSelfhostedProviderNotFoundError(error);
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
- if (!error) {
21738
- return false;
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
- const status = error.status ?? error.statusCode;
21741
- if (status === 404) {
21742
- return true;
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
- const name = typeof error.name === "string" ? error.name : "";
21745
- const code = typeof error.code === "string" ? error.code : "";
21746
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : String(error?.message ?? "");
21747
- const haystack = `${name} ${code} ${message}`.toLowerCase();
21748
- const goneMarkers = [
21749
- "not found",
21750
- "no longer running",
21751
- "no longer exists",
21752
- "does not exist",
21753
- "doesn't exist",
21754
- "has been terminated",
21755
- "was terminated",
21756
- "is terminated",
21757
- "sandbox terminated",
21758
- "notfound",
21759
- "sandbox_not_found",
21760
- "box no longer running"
21761
- ];
21762
- if (haystack.includes("already running") || haystack.includes("still running") || haystack.includes("already exists")) {
21763
- return false;
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 workspaceArchives = readWorkspaceArchivePairFromEnvelopeSessionState(envelopeSessionState);
21804
- const workspaceArchive = workspaceArchives.current;
21805
- const coldRestore = async (resumeFallbackState, skipWorkspaceHydrate = false) => {
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 (!skipWorkspaceHydrate && (workspaceArchive || workspaceArchives.previous)) {
23439
+ if (workspaceArchive) {
21833
23440
  const hydrate = restored.hydrateWorkspace;
21834
- if (typeof hydrate === "function") {
21835
- let hydrated = false;
21836
- for (const candidate of [
21837
- { archive: workspaceArchive, label: "current" },
21838
- { archive: workspaceArchives.previous, label: "previous" }
21839
- ]) {
21840
- if (!candidate.archive) {
21841
- continue;
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 hydrate.call(restored, candidate.archive);
21845
- console.info(
21846
- `[sandbox] cold-restore hydrated workspace from ${candidate.label} archive`
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
- if (!hydrated) {
21858
- await terminateCreatedSandbox(client, restored, restoredState);
21859
- return await coldRestore(resumeFallbackState, true);
21860
- }
21861
- hydrationApplied = true;
21862
- const hydratedState = restored.state;
21863
- const hydratedInstanceId = readInstanceId(restored);
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
- isProviderSandboxNotFoundError2 as isProviderSandboxNotFoundError,
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-65UDUFUO.js.map
23848
+ //# sourceMappingURL=chunk-JGUQSAG5.js.map