@opengeni/runtime 0.11.0 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YUAFQEFY.js → chunk-JGUQSAG5.js} +2605 -279
- package/dist/chunk-JGUQSAG5.js.map +1 -0
- package/dist/chunk-SSB7VOLE.js +301 -0
- package/dist/chunk-SSB7VOLE.js.map +1 -0
- package/dist/{index-D6yi-JcU.d.ts → index-DU-KXVlx.d.ts} +364 -31
- package/dist/index.d.ts +103 -10
- package/dist/index.js +756 -761
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +80 -0
- package/dist/mcp-network.js +66 -0
- package/dist/mcp-network.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +39 -1
- package/package.json +9 -4
- package/src/codex-tool-search.ts +20 -1
- package/src/index.ts +502 -85
- package/src/mcp-network.ts +343 -0
- package/src/sandbox/channel-a.ts +92 -71
- package/src/sandbox/display-stack.ts +860 -23
- package/src/sandbox/exec-banner.ts +82 -0
- package/src/sandbox/index.ts +367 -145
- package/src/sandbox/provider-errors.ts +175 -0
- package/src/sandbox/providers/modal.ts +29 -2
- package/src/sandbox/routing/backend-resolver.ts +15 -0
- package/src/sandbox/routing/routing-session.ts +730 -36
- package/src/sandbox/select.ts +8 -0
- package/src/sandbox/turn-tool-cancellation.ts +366 -92
- package/src/sandbox/workspace-archive.ts +289 -0
- package/src/sandbox-computer.ts +3 -1
- package/src/usage-telemetry.ts +495 -51
- package/dist/chunk-YUAFQEFY.js.map +0 -1
|
@@ -13274,8 +13274,8 @@ async function sweepModalOrphanSandboxes(settings, liveLeases, options = {}) {
|
|
|
13274
13274
|
if (liveByInstance) {
|
|
13275
13275
|
if (leaseId !== liveByInstance.leaseId || workspaceId !== liveByInstance.workspaceId || sandboxGroupId !== liveByInstance.sandboxGroupId) {
|
|
13276
13276
|
try {
|
|
13277
|
-
const
|
|
13278
|
-
await
|
|
13277
|
+
const sandbox2 = await modal.sandboxes.fromId(info.id);
|
|
13278
|
+
await sandbox2.setTags(
|
|
13279
13279
|
modalSandboxAttributionTags({
|
|
13280
13280
|
leaseId: liveByInstance.leaseId,
|
|
13281
13281
|
workspaceId: liveByInstance.workspaceId,
|
|
@@ -13306,10 +13306,28 @@ async function sweepModalOrphanSandboxes(settings, liveLeases, options = {}) {
|
|
|
13306
13306
|
skipped += 1;
|
|
13307
13307
|
continue;
|
|
13308
13308
|
}
|
|
13309
|
+
const candidate = { sandboxId: info.id, reason, tags };
|
|
13310
|
+
let sandbox;
|
|
13311
|
+
try {
|
|
13312
|
+
sandbox = await modal.sandboxes.fromId(info.id);
|
|
13313
|
+
} catch {
|
|
13314
|
+
skipped += 1;
|
|
13315
|
+
continue;
|
|
13316
|
+
}
|
|
13317
|
+
if (options.revalidateTermination) {
|
|
13318
|
+
try {
|
|
13319
|
+
if (!await options.revalidateTermination(candidate)) {
|
|
13320
|
+
skipped += 1;
|
|
13321
|
+
continue;
|
|
13322
|
+
}
|
|
13323
|
+
} catch {
|
|
13324
|
+
skipped += 1;
|
|
13325
|
+
continue;
|
|
13326
|
+
}
|
|
13327
|
+
}
|
|
13309
13328
|
try {
|
|
13310
|
-
const sandbox = await modal.sandboxes.fromId(info.id);
|
|
13311
13329
|
await sandbox.terminate();
|
|
13312
|
-
terminated.push(
|
|
13330
|
+
terminated.push(candidate);
|
|
13313
13331
|
} catch {
|
|
13314
13332
|
skipped += 1;
|
|
13315
13333
|
}
|
|
@@ -14823,11 +14841,11 @@ var NatsControlRpc = class {
|
|
|
14823
14841
|
}
|
|
14824
14842
|
}
|
|
14825
14843
|
};
|
|
14826
|
-
function offlineControlResponse(
|
|
14827
|
-
return { requestId, error: offlineAgentError(void 0, neverSent), result: void 0 };
|
|
14844
|
+
function offlineControlResponse(requestId2, neverSent = false) {
|
|
14845
|
+
return { requestId: requestId2, error: offlineAgentError(void 0, neverSent), result: void 0 };
|
|
14828
14846
|
}
|
|
14829
|
-
function timeoutControlResponse(
|
|
14830
|
-
return { requestId, error: timeoutAgentError(), result: void 0 };
|
|
14847
|
+
function timeoutControlResponse(requestId2) {
|
|
14848
|
+
return { requestId: requestId2, error: timeoutAgentError(), result: void 0 };
|
|
14831
14849
|
}
|
|
14832
14850
|
|
|
14833
14851
|
// src/sandbox/selfhosted/session.ts
|
|
@@ -15407,10 +15425,10 @@ var OpConsumer = class {
|
|
|
15407
15425
|
* `SelfhostedControlError` on any AgentError. `requestId` is overridable
|
|
15408
15426
|
* because OpStart's REQUEST ID IS THE OP ID (its idempotency key) and must
|
|
15409
15427
|
* be STABLE across retries — every other op uses a fresh UUID per attempt. */
|
|
15410
|
-
async controlOp(op,
|
|
15428
|
+
async controlOp(op, requestId2 = crypto.randomUUID()) {
|
|
15411
15429
|
const res = await this.deps.controlRpc.request(
|
|
15412
15430
|
this.deps.rpcSubject,
|
|
15413
|
-
{ requestId, epoch: this.deps.epoch, op },
|
|
15431
|
+
{ requestId: requestId2, epoch: this.deps.epoch, op },
|
|
15414
15432
|
{ timeoutMs: this.deps.controlTimeoutMs }
|
|
15415
15433
|
);
|
|
15416
15434
|
if (res.error || !res.result) {
|
|
@@ -17474,6 +17492,295 @@ function assertProviderRegistryInvariants() {
|
|
|
17474
17492
|
}
|
|
17475
17493
|
assertProviderRegistryInvariants();
|
|
17476
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
|
+
|
|
17477
17784
|
// src/sandbox/index.ts
|
|
17478
17785
|
import { collectSandboxEnvironment as collectSandboxEnvironment2, parseExposedPorts as parseExposedPorts2 } from "@opengeni/config";
|
|
17479
17786
|
|
|
@@ -17690,6 +17997,9 @@ function negotiateCapabilities(ctx) {
|
|
|
17690
17997
|
os: ctx.os,
|
|
17691
17998
|
liveness: ctx.liveness,
|
|
17692
17999
|
leaseEpoch: ctx.leaseEpoch,
|
|
18000
|
+
workspaceGeneration: ctx.workspaceGeneration ?? null,
|
|
18001
|
+
archiveGeneration: ctx.archiveGeneration ?? null,
|
|
18002
|
+
archiveComplete: ctx.archiveComplete ?? false,
|
|
17693
18003
|
viewerHeartbeatIntervalMs: 3e4,
|
|
17694
18004
|
FileSystem: fileSystem,
|
|
17695
18005
|
Terminal: terminal,
|
|
@@ -17732,8 +18042,67 @@ async function verifyStreamToken(secret, token, nowSeconds = Math.floor(Date.now
|
|
|
17732
18042
|
|
|
17733
18043
|
// src/sandbox/display-stack.ts
|
|
17734
18044
|
import { DESKTOP_STREAM_PORT as DESKTOP_STREAM_PORT4 } from "@opengeni/contracts";
|
|
18045
|
+
|
|
18046
|
+
// src/sandbox/exec-banner.ts
|
|
18047
|
+
var EXEC_BANNER_HEADER_MAX_CHARS = 16 * 1024;
|
|
18048
|
+
var OUTPUT_DELIMITER = /\r?\nOutput:\r?\n/u;
|
|
18049
|
+
var SDK_METADATA_LINE = /^(?:Chunk ID:|Wall time:|Original token count:|Output:|Process (?:running with session ID|exited with code))/mu;
|
|
18050
|
+
var RUNNING_LINE = /^Process running with session ID (\d+)$/u;
|
|
18051
|
+
var EXITED_LINE = /^Process exited with code (-?\d+)$/u;
|
|
18052
|
+
var PROCESS_STATUS_PREFIX = /^Process (?:running with session ID|exited with code)/u;
|
|
18053
|
+
function parseExecResponseBanner(raw) {
|
|
18054
|
+
const delimiter = OUTPUT_DELIMITER.exec(raw);
|
|
18055
|
+
if (!delimiter) {
|
|
18056
|
+
return SDK_METADATA_LINE.test(raw) ? { kind: "invalid" } : { kind: "absent" };
|
|
18057
|
+
}
|
|
18058
|
+
if (delimiter.index > EXEC_BANNER_HEADER_MAX_CHARS) return { kind: "invalid" };
|
|
18059
|
+
const header = raw.slice(0, delimiter.index);
|
|
18060
|
+
const status = [];
|
|
18061
|
+
for (const line of header.split(/\r?\n/u)) {
|
|
18062
|
+
const running = RUNNING_LINE.exec(line);
|
|
18063
|
+
if (running) {
|
|
18064
|
+
status.push({ kind: "running", value: running[1] });
|
|
18065
|
+
continue;
|
|
18066
|
+
}
|
|
18067
|
+
const exited = EXITED_LINE.exec(line);
|
|
18068
|
+
if (exited) {
|
|
18069
|
+
status.push({ kind: "exited", value: exited[1] });
|
|
18070
|
+
continue;
|
|
18071
|
+
}
|
|
18072
|
+
if (PROCESS_STATUS_PREFIX.test(line)) return { kind: "invalid" };
|
|
18073
|
+
}
|
|
18074
|
+
if (status.length !== 1) return { kind: "invalid" };
|
|
18075
|
+
const terminal = status[0];
|
|
18076
|
+
const value = Number(terminal.value);
|
|
18077
|
+
if (!Number.isSafeInteger(value)) return { kind: "invalid" };
|
|
18078
|
+
if (terminal.kind === "running") {
|
|
18079
|
+
return value >= 0 ? { kind: "running", sessionId: value } : { kind: "invalid" };
|
|
18080
|
+
}
|
|
18081
|
+
return { kind: "exited", exitCode: value };
|
|
18082
|
+
}
|
|
18083
|
+
function parseExecBannerSessionId(raw) {
|
|
18084
|
+
const banner = parseExecResponseBanner(raw);
|
|
18085
|
+
return banner.kind === "running" ? banner.sessionId : null;
|
|
18086
|
+
}
|
|
18087
|
+
function parseExecBannerExitCode(raw) {
|
|
18088
|
+
const banner = parseExecResponseBanner(raw);
|
|
18089
|
+
return banner.kind === "exited" ? banner.exitCode : null;
|
|
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
|
+
}
|
|
18097
|
+
|
|
18098
|
+
// src/sandbox/display-stack.ts
|
|
17735
18099
|
var STREAM_PORT = DESKTOP_STREAM_PORT4;
|
|
17736
18100
|
var DISPLAY_STACK_TIMEOUT_MS = 9e4;
|
|
18101
|
+
var DISPLAY_STACK_PROVIDER_YIELD_MS = 15e3;
|
|
18102
|
+
var DISPLAY_STACK_PROVIDER_POLL_MS = 5e3;
|
|
18103
|
+
var DISPLAY_STACK_TERMINATION_GRACE_MS = 5e3;
|
|
18104
|
+
var DISPLAY_STACK_CLEANUP_CALL_MS = 250;
|
|
18105
|
+
var DISPLAY_STACK_DRAIN_GRACE_MS = 100;
|
|
17737
18106
|
var PAINT_PROBE_ATTEMPTS = 150;
|
|
17738
18107
|
var PAINT_PROBE_INTERVAL_S = 0.2;
|
|
17739
18108
|
var PAINT_MIN_BYTES = 6e4;
|
|
@@ -17743,7 +18112,7 @@ var DisplayStackError = class extends Error {
|
|
|
17743
18112
|
exitCode;
|
|
17744
18113
|
stage;
|
|
17745
18114
|
constructor(exitCode, output) {
|
|
17746
|
-
const stage = exitCode === 11 ? "xvfb" : exitCode === 12 ? "x11vnc" : exitCode === 13 ? "websockify" : exitCode === 14 ? "paint" : "unknown";
|
|
18115
|
+
const stage = exitCode === 11 ? "xvfb" : exitCode === 12 ? "x11vnc" : exitCode === 13 ? "websockify" : exitCode === 14 ? "paint" : exitCode === 15 ? "lock" : exitCode === 124 || /OPENGENI_DISPLAY_TIMEOUT/u.test(output) ? "timeout" : "unknown";
|
|
17747
18116
|
super(
|
|
17748
18117
|
`desktop display stack failed at stage "${stage}" (exit ${exitCode})${output ? `:
|
|
17749
18118
|
${output}` : ""}`
|
|
@@ -17763,9 +18132,11 @@ function buildDisplayStackScript(options = {}) {
|
|
|
17763
18132
|
const geometry = options.geometry ?? DEFAULT_DESKTOP_GEOMETRY;
|
|
17764
18133
|
const port = options.port ?? DESKTOP_STREAM_PORT4;
|
|
17765
18134
|
const env = `DESKTOP_W=${geometry.width} DESKTOP_H=${geometry.height} DESKTOP_DPI=${geometry.dpi} STREAM_PORT=${port}`;
|
|
17766
|
-
const
|
|
17767
|
-
const
|
|
17768
|
-
|
|
18135
|
+
const lockedLauncher = `ds_started=$1; fw=$2; ds_class=cold; ds_stage() { ds_now=$(date +%s%3N); echo "OPENGENI_DISPLAY_STAGE stage=$1 elapsed_ms=$((ds_now-ds_started)) classification=$ds_class"; }; fa=$(date +%s%3N); fd=$((fa-fw)); [ "$fd" -ge 50 ] && ds_class=contention; ds_stage outer_flock_acquired; ds_stage launcher_issued; env ${env} opengeni-desktop-up; urc=$?; ds_stage launcher_complete; exit "$urc"`;
|
|
18136
|
+
const bringUp = `if nc -z 127.0.0.1 ${port} >/dev/null 2>&1 && nc -z 127.0.0.1 5900 >/dev/null 2>&1; then ds_class=already_ready; ds_stage precheck_ready; echo "OPENGENI_DESKTOP_UP port=${port} geometry=${geometry.width}x${geometry.height} dpi=${geometry.dpi} (precheck)"; else ds_class=cold; ds_stage precheck_miss; mkdir -p /tmp/opengeni-desktop; ds_stage outer_flock_wait; fw=$(date +%s%3N); flock --close --conflict-exit-code 75 --wait 45 /tmp/opengeni-desktop/up.outer.lock bash -c ${shellQuote3(lockedLauncher)} _ "$ds_started" "$fw"; frc=$?; if [ "$frc" -eq 75 ]; then ds_stage outer_flock_timeout; echo "OPENGENI_DESKTOP_LOCK_TIMEOUT"; exit 15; fi; if [ "$frc" -ne 0 ]; then exit "$frc"; fi; fi`;
|
|
18137
|
+
const paintProbe = `ds_stage paint_probe_started; p=/tmp/opengeni-desktop/paint-probe.png; prev=0; for i in $(seq 1 ${PAINT_PROBE_ATTEMPTS}); do if DISPLAY=:0 scrot -o "$p" >/dev/null 2>&1; then sz=$(wc -c < "$p" 2>/dev/null || echo 0); else sz=0; fi; rm -f "$p"; if [ "$sz" -ge ${PAINT_MIN_BYTES} ] && [ "$prev" -ge ${PAINT_MIN_BYTES} ]; then d=$((sz-prev)); [ "$d" -lt 0 ] && d=$((0-d)); if [ "$d" -le ${PAINT_SETTLE_DELTA_BYTES} ]; then ds_stage paint_ready; break; fi; fi; prev=$sz; if [ "$i" = "${PAINT_PROBE_ATTEMPTS}" ]; then ds_stage paint_failed; echo "OPENGENI_DESKTOP_NOT_PAINTING scrot below ${PAINT_MIN_BYTES}B or unsettled after warmup (last=$sz)"; exit 14; fi; sleep ${PAINT_PROBE_INTERVAL_S}; done`;
|
|
18138
|
+
const trace = `ds_started=$(date +%s%3N); ds_class=unknown; ds_stage() { ds_now=$(date +%s%3N); echo "OPENGENI_DISPLAY_STAGE stage=$1 elapsed_ms=$((ds_now-ds_started)) classification=$ds_class"; }; ds_stage script_entry`;
|
|
18139
|
+
return `${trace}; mkdir -p /tmp/opengeni-desktop; { ${bringUp} ; } && { ${paintProbe} ; }`;
|
|
17769
18140
|
}
|
|
17770
18141
|
function execResultOutput(result) {
|
|
17771
18142
|
if (typeof result === "string") {
|
|
@@ -17775,11 +18146,417 @@ function execResultOutput(result) {
|
|
|
17775
18146
|
}
|
|
17776
18147
|
function execResultExitCode(result) {
|
|
17777
18148
|
if (typeof result === "string") {
|
|
17778
|
-
|
|
18149
|
+
const banner = parseExecResponseBanner(result);
|
|
18150
|
+
if (banner.kind === "exited") return banner.exitCode;
|
|
18151
|
+
return banner.kind === "invalid" ? -1 : null;
|
|
17779
18152
|
}
|
|
17780
18153
|
return typeof result.exitCode === "number" ? result.exitCode : null;
|
|
17781
18154
|
}
|
|
18155
|
+
function execResultSessionId(result) {
|
|
18156
|
+
if (typeof result === "string") {
|
|
18157
|
+
const banner = parseExecResponseBanner(result);
|
|
18158
|
+
return banner.kind === "running" ? banner.sessionId : null;
|
|
18159
|
+
}
|
|
18160
|
+
const value = result.sessionId ?? result.session_id;
|
|
18161
|
+
return typeof value === "number" ? value : null;
|
|
18162
|
+
}
|
|
18163
|
+
function shellQuote3(value) {
|
|
18164
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
18165
|
+
}
|
|
18166
|
+
function boundedDisplayStackCommand(command, timeoutMs, markerPath, markerToken) {
|
|
18167
|
+
const graceMs = Math.min(
|
|
18168
|
+
DISPLAY_STACK_TERMINATION_GRACE_MS,
|
|
18169
|
+
Math.max(20, Math.floor(timeoutMs / 4))
|
|
18170
|
+
);
|
|
18171
|
+
const commandTimeoutMs = Math.max(1, timeoutMs - graceMs);
|
|
18172
|
+
const killAfterMs = Math.max(1, Math.floor(graceMs / 2));
|
|
18173
|
+
const commandTimeoutSeconds = (commandTimeoutMs / 1e3).toFixed(3);
|
|
18174
|
+
const killAfterSeconds = (killAfterMs / 1e3).toFixed(3);
|
|
18175
|
+
const managedCommand = `marker=${shellQuote3(markerPath)}; token=${shellQuote3(markerToken)}; cancel="$marker.cancelled"; if [ -e "$cancel" ]; then echo "OPENGENI_DISPLAY_CANCELLED"; exit 124; fi; pid=$$; pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' '); start=$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true); printf '%s %s %s %s\\n' "$pid" "$pgid" "$start" "$token" >"$marker"; if [ -e "$cancel" ]; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CANCELLED"; exit 124; fi; bash -c ${shellQuote3(command)}; rc=$?; rm -f "$marker"; exit "$rc"`;
|
|
18176
|
+
return (
|
|
18177
|
+
// Do not use timeout --foreground here. The display command contains flock
|
|
18178
|
+
// supervisors and launcher children; default process-group mode is required
|
|
18179
|
+
// so a deadline cannot leave a waiter alive to acquire the lock and launch
|
|
18180
|
+
// after the caller has already failed.
|
|
18181
|
+
`timeout --signal=TERM --kill-after=${killAfterSeconds}s ${commandTimeoutSeconds}s bash -c ${shellQuote3(managedCommand)}; rc=$?; if [ "$rc" -eq 124 ]; then echo "OPENGENI_DISPLAY_TIMEOUT elapsed_ms=${commandTimeoutMs}"; fi; exit "$rc"`
|
|
18182
|
+
);
|
|
18183
|
+
}
|
|
18184
|
+
var DisplayStackWallTimeoutError = class extends Error {
|
|
18185
|
+
};
|
|
18186
|
+
var DisplayStackOperationFencedError = class extends Error {
|
|
18187
|
+
};
|
|
18188
|
+
var stableDisplayStackOperationStates = /* @__PURE__ */ new Map();
|
|
18189
|
+
var stableSessionStateOperationStates = /* @__PURE__ */ new WeakMap();
|
|
18190
|
+
var displayStackOperationStates = /* @__PURE__ */ new WeakMap();
|
|
18191
|
+
function asRecord(value) {
|
|
18192
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
18193
|
+
}
|
|
18194
|
+
function nonEmptyString(value) {
|
|
18195
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
18196
|
+
}
|
|
18197
|
+
function readSessionState(session) {
|
|
18198
|
+
try {
|
|
18199
|
+
return asRecord(session)?.state;
|
|
18200
|
+
} catch {
|
|
18201
|
+
return void 0;
|
|
18202
|
+
}
|
|
18203
|
+
}
|
|
18204
|
+
function stableSandboxIdentity(session, options) {
|
|
18205
|
+
const explicit = nonEmptyString(options.telemetryContext?.sandboxId);
|
|
18206
|
+
if (explicit) return `sandbox:${explicit}`;
|
|
18207
|
+
const sessionRecord = asRecord(session);
|
|
18208
|
+
const state = asRecord(readSessionState(session));
|
|
18209
|
+
const providerState = state ? asRecord(state.providerState) : null;
|
|
18210
|
+
const candidates = [sessionRecord, state, providerState];
|
|
18211
|
+
const fields = [
|
|
18212
|
+
"sandboxId",
|
|
18213
|
+
"instanceId",
|
|
18214
|
+
"agentId",
|
|
18215
|
+
"hostId",
|
|
18216
|
+
"containerId",
|
|
18217
|
+
"id",
|
|
18218
|
+
"workspaceRootPath"
|
|
18219
|
+
];
|
|
18220
|
+
for (const candidate of candidates) {
|
|
18221
|
+
if (!candidate) continue;
|
|
18222
|
+
for (const field of fields) {
|
|
18223
|
+
const value = nonEmptyString(candidate[field]);
|
|
18224
|
+
if (value) return `sandbox:${value}`;
|
|
18225
|
+
}
|
|
18226
|
+
}
|
|
18227
|
+
return null;
|
|
18228
|
+
}
|
|
18229
|
+
function newOperationState() {
|
|
18230
|
+
return {
|
|
18231
|
+
epoch: 0,
|
|
18232
|
+
owner: null,
|
|
18233
|
+
waiters: [],
|
|
18234
|
+
inFlight: /* @__PURE__ */ new Set(),
|
|
18235
|
+
drain: null,
|
|
18236
|
+
pendingCleanup: /* @__PURE__ */ new Map(),
|
|
18237
|
+
physicallyQuiescent: true
|
|
18238
|
+
};
|
|
18239
|
+
}
|
|
18240
|
+
function operationState(session, options) {
|
|
18241
|
+
if ((typeof session !== "object" || session === null) && typeof session !== "function") {
|
|
18242
|
+
return null;
|
|
18243
|
+
}
|
|
18244
|
+
const stableKey = stableSandboxIdentity(session, options);
|
|
18245
|
+
if (stableKey) {
|
|
18246
|
+
let state2 = stableDisplayStackOperationStates.get(stableKey);
|
|
18247
|
+
if (!state2) {
|
|
18248
|
+
state2 = newOperationState();
|
|
18249
|
+
stableDisplayStackOperationStates.set(stableKey, state2);
|
|
18250
|
+
}
|
|
18251
|
+
return state2;
|
|
18252
|
+
}
|
|
18253
|
+
const providerState = readSessionState(session);
|
|
18254
|
+
if (providerState && (typeof providerState === "object" || typeof providerState === "function")) {
|
|
18255
|
+
const stateKey = providerState;
|
|
18256
|
+
let state2 = stableSessionStateOperationStates.get(stateKey);
|
|
18257
|
+
if (!state2) {
|
|
18258
|
+
state2 = newOperationState();
|
|
18259
|
+
stableSessionStateOperationStates.set(stateKey, state2);
|
|
18260
|
+
}
|
|
18261
|
+
return state2;
|
|
18262
|
+
}
|
|
18263
|
+
const key = session;
|
|
18264
|
+
let state = displayStackOperationStates.get(key);
|
|
18265
|
+
if (!state) {
|
|
18266
|
+
state = newOperationState();
|
|
18267
|
+
displayStackOperationStates.set(key, state);
|
|
18268
|
+
}
|
|
18269
|
+
return state;
|
|
18270
|
+
}
|
|
18271
|
+
function settleOperationWaiter(waiter, error) {
|
|
18272
|
+
if (waiter.settled) return;
|
|
18273
|
+
waiter.settled = true;
|
|
18274
|
+
if (waiter.timer) clearTimeout(waiter.timer);
|
|
18275
|
+
if (error === void 0) waiter.resolve();
|
|
18276
|
+
else waiter.reject(error);
|
|
18277
|
+
}
|
|
18278
|
+
function pumpOperationWaiters(state) {
|
|
18279
|
+
if (state.owner) return;
|
|
18280
|
+
while (state.waiters.length > 0) {
|
|
18281
|
+
const waiter = state.waiters.shift();
|
|
18282
|
+
if (waiter.settled) continue;
|
|
18283
|
+
if (waiter.epoch !== state.epoch) {
|
|
18284
|
+
settleOperationWaiter(waiter, new DisplayStackOperationFencedError());
|
|
18285
|
+
continue;
|
|
18286
|
+
}
|
|
18287
|
+
state.owner = waiter;
|
|
18288
|
+
settleOperationWaiter(waiter);
|
|
18289
|
+
return;
|
|
18290
|
+
}
|
|
18291
|
+
}
|
|
18292
|
+
function acquireOperation(state, deadline) {
|
|
18293
|
+
return new Promise((resolve5, reject) => {
|
|
18294
|
+
const waiter = {
|
|
18295
|
+
epoch: state.epoch,
|
|
18296
|
+
settled: false,
|
|
18297
|
+
resolve: () => {
|
|
18298
|
+
resolve5(() => {
|
|
18299
|
+
if (state.owner !== waiter) return;
|
|
18300
|
+
state.owner = null;
|
|
18301
|
+
pumpOperationWaiters(state);
|
|
18302
|
+
});
|
|
18303
|
+
},
|
|
18304
|
+
reject
|
|
18305
|
+
};
|
|
18306
|
+
state.waiters.push(waiter);
|
|
18307
|
+
const remainingMs = deadline - monotonicNow();
|
|
18308
|
+
if (remainingMs <= 0) {
|
|
18309
|
+
waiter.settled = true;
|
|
18310
|
+
state.waiters = state.waiters.filter((candidate) => candidate !== waiter);
|
|
18311
|
+
reject(new DisplayStackWallTimeoutError());
|
|
18312
|
+
return;
|
|
18313
|
+
}
|
|
18314
|
+
waiter.timer = setTimeout(() => {
|
|
18315
|
+
if (waiter.settled) return;
|
|
18316
|
+
waiter.settled = true;
|
|
18317
|
+
state.waiters = state.waiters.filter((candidate) => candidate !== waiter);
|
|
18318
|
+
reject(new DisplayStackWallTimeoutError());
|
|
18319
|
+
}, remainingMs);
|
|
18320
|
+
waiter.timer.unref?.();
|
|
18321
|
+
pumpOperationWaiters(state);
|
|
18322
|
+
});
|
|
18323
|
+
}
|
|
18324
|
+
function invalidateQueuedOperations(state, epoch) {
|
|
18325
|
+
const retained = [];
|
|
18326
|
+
for (const waiter of state.waiters) {
|
|
18327
|
+
if (waiter.epoch === epoch)
|
|
18328
|
+
settleOperationWaiter(waiter, new DisplayStackOperationFencedError());
|
|
18329
|
+
else retained.push(waiter);
|
|
18330
|
+
}
|
|
18331
|
+
state.waiters = retained;
|
|
18332
|
+
}
|
|
18333
|
+
function monotonicNow() {
|
|
18334
|
+
return performance.now();
|
|
18335
|
+
}
|
|
18336
|
+
async function waitForFencedOperation(state, deadline) {
|
|
18337
|
+
const drain = state.drain;
|
|
18338
|
+
if (!drain) return true;
|
|
18339
|
+
try {
|
|
18340
|
+
await beforeDeadline(drain, deadline);
|
|
18341
|
+
} catch {
|
|
18342
|
+
return false;
|
|
18343
|
+
}
|
|
18344
|
+
if (state.drain === drain) state.drain = null;
|
|
18345
|
+
return true;
|
|
18346
|
+
}
|
|
18347
|
+
function trackProviderCall(state, promise) {
|
|
18348
|
+
if (!state) return promise;
|
|
18349
|
+
state.inFlight.add(promise);
|
|
18350
|
+
void promise.then(
|
|
18351
|
+
() => state.inFlight.delete(promise),
|
|
18352
|
+
() => state.inFlight.delete(promise)
|
|
18353
|
+
);
|
|
18354
|
+
return promise;
|
|
18355
|
+
}
|
|
18356
|
+
function boundedProviderDrain(pending) {
|
|
18357
|
+
if (pending.length === 0) return Promise.resolve();
|
|
18358
|
+
return new Promise((resolve5) => {
|
|
18359
|
+
let remaining = pending.length;
|
|
18360
|
+
let timer;
|
|
18361
|
+
const finish = () => {
|
|
18362
|
+
if (timer) clearTimeout(timer);
|
|
18363
|
+
resolve5();
|
|
18364
|
+
};
|
|
18365
|
+
timer = setTimeout(finish, DISPLAY_STACK_DRAIN_GRACE_MS);
|
|
18366
|
+
timer.unref?.();
|
|
18367
|
+
for (const promise of pending) {
|
|
18368
|
+
void promise.then(
|
|
18369
|
+
() => {
|
|
18370
|
+
remaining -= 1;
|
|
18371
|
+
if (remaining === 0) finish();
|
|
18372
|
+
},
|
|
18373
|
+
() => {
|
|
18374
|
+
remaining -= 1;
|
|
18375
|
+
if (remaining === 0) finish();
|
|
18376
|
+
}
|
|
18377
|
+
);
|
|
18378
|
+
}
|
|
18379
|
+
});
|
|
18380
|
+
}
|
|
18381
|
+
function fenceOperation(state, epoch, abortController, cleanup) {
|
|
18382
|
+
abortController.abort(new DisplayStackWallTimeoutError());
|
|
18383
|
+
if (!state) return;
|
|
18384
|
+
if (state.epoch !== epoch) return;
|
|
18385
|
+
state.pendingCleanup.set(cleanup.markerToken, cleanup);
|
|
18386
|
+
state.physicallyQuiescent = false;
|
|
18387
|
+
invalidateQueuedOperations(state, epoch);
|
|
18388
|
+
state.epoch += 1;
|
|
18389
|
+
const pending = [...state.inFlight];
|
|
18390
|
+
const drain = boundedProviderDrain(pending);
|
|
18391
|
+
state.drain = state.drain ? Promise.all([state.drain, drain]).then(() => void 0) : drain;
|
|
18392
|
+
void drain.then(() => {
|
|
18393
|
+
for (const promise of pending) state.inFlight.delete(promise);
|
|
18394
|
+
});
|
|
18395
|
+
}
|
|
18396
|
+
function assertCurrentOperation(state, epoch) {
|
|
18397
|
+
if (state && state.epoch !== epoch) throw new DisplayStackOperationFencedError();
|
|
18398
|
+
}
|
|
18399
|
+
async function beforeDeadline(promise, deadline, now = monotonicNow) {
|
|
18400
|
+
const remainingMs = deadline - now();
|
|
18401
|
+
if (remainingMs <= 0) throw new DisplayStackWallTimeoutError();
|
|
18402
|
+
let timer;
|
|
18403
|
+
try {
|
|
18404
|
+
return await Promise.race([
|
|
18405
|
+
promise,
|
|
18406
|
+
new Promise((_resolve, reject) => {
|
|
18407
|
+
timer = setTimeout(() => reject(new DisplayStackWallTimeoutError()), remainingMs);
|
|
18408
|
+
timer.unref?.();
|
|
18409
|
+
})
|
|
18410
|
+
]);
|
|
18411
|
+
} finally {
|
|
18412
|
+
if (timer) clearTimeout(timer);
|
|
18413
|
+
}
|
|
18414
|
+
}
|
|
18415
|
+
function displayStackCleanupCommand(markerPath, markerToken) {
|
|
18416
|
+
const marker = shellQuote3(markerPath);
|
|
18417
|
+
const token = shellQuote3(markerToken);
|
|
18418
|
+
const script = `marker=${marker}; token=${token}; cancel="$marker.cancelled"; : >"$cancel" || { echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75; }; if [ ! -r "$marker" ]; then echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; read -r pid pgid start actual <"$marker" || { echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75; }; [ "$actual" = "$token" ] || { echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75; }; case "$pid" in ''|*[!0-9]*) echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75;; esac; case "$pgid" in ''|*[!0-9]*) echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75;; esac; case "$start" in ''|*[!0-9]*) echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75;; esac; current=$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true); if [ -z "$current" ] || [ "$current" != "$start" ]; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; kill -TERM -- "-$pgid" 2>/dev/null || true; for i in $(seq 1 10); do if ! kill -0 -- "-$pgid" 2>/dev/null; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; sleep 0.05; done; kill -KILL -- "-$pgid" 2>/dev/null || true; for i in $(seq 1 20); do if ! kill -0 -- "-$pgid" 2>/dev/null; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; sleep 0.05; done; echo "OPENGENI_DISPLAY_CLEANUP status=alive"; exit 75`;
|
|
18419
|
+
return `timeout --signal=TERM --kill-after=0.2s 1.5s bash -c ${shellQuote3(script)}`;
|
|
18420
|
+
}
|
|
18421
|
+
async function runProviderCommandToDeadline(session, command, deadline) {
|
|
18422
|
+
const controller = new AbortController();
|
|
18423
|
+
let result = await beforeDeadline(
|
|
18424
|
+
Promise.resolve(
|
|
18425
|
+
typeof session.exec === "function" ? session.exec({
|
|
18426
|
+
cmd: command,
|
|
18427
|
+
yieldTimeMs: Math.min(500, DISPLAY_STACK_CLEANUP_CALL_MS),
|
|
18428
|
+
maxOutputTokens: 4e3,
|
|
18429
|
+
signal: controller.signal
|
|
18430
|
+
}) : session.execCommand({
|
|
18431
|
+
cmd: command,
|
|
18432
|
+
yieldTimeMs: Math.min(500, DISPLAY_STACK_CLEANUP_CALL_MS),
|
|
18433
|
+
maxOutputTokens: 4e3,
|
|
18434
|
+
signal: controller.signal
|
|
18435
|
+
})
|
|
18436
|
+
),
|
|
18437
|
+
deadline
|
|
18438
|
+
);
|
|
18439
|
+
let providerSessionId = execResultSessionId(result);
|
|
18440
|
+
while (providerSessionId !== null) {
|
|
18441
|
+
if (typeof session.writeStdin !== "function") {
|
|
18442
|
+
throw new Error("display cleanup provider yielded without writeStdin");
|
|
18443
|
+
}
|
|
18444
|
+
const remainingMs = deadline - monotonicNow();
|
|
18445
|
+
if (remainingMs <= 1) throw new DisplayStackWallTimeoutError();
|
|
18446
|
+
result = await beforeDeadline(
|
|
18447
|
+
session.writeStdin({
|
|
18448
|
+
sessionId: providerSessionId,
|
|
18449
|
+
chars: "",
|
|
18450
|
+
yieldTimeMs: Math.max(1, Math.min(250, remainingMs - 1)),
|
|
18451
|
+
maxOutputTokens: 4e3,
|
|
18452
|
+
signal: controller.signal
|
|
18453
|
+
}),
|
|
18454
|
+
deadline
|
|
18455
|
+
);
|
|
18456
|
+
providerSessionId = execResultSessionId(result);
|
|
18457
|
+
}
|
|
18458
|
+
return result;
|
|
18459
|
+
}
|
|
18460
|
+
async function interruptProviderProcess(session, providerSessionId, deadline) {
|
|
18461
|
+
if (providerSessionId === null || typeof session.writeStdin !== "function") return;
|
|
18462
|
+
try {
|
|
18463
|
+
await beforeDeadline(
|
|
18464
|
+
session.writeStdin({
|
|
18465
|
+
sessionId: providerSessionId,
|
|
18466
|
+
chars: "",
|
|
18467
|
+
yieldTimeMs: 100,
|
|
18468
|
+
maxOutputTokens: 256
|
|
18469
|
+
}),
|
|
18470
|
+
deadline
|
|
18471
|
+
);
|
|
18472
|
+
} catch {
|
|
18473
|
+
}
|
|
18474
|
+
}
|
|
18475
|
+
async function cleanupDisplayStackProcess(session, pending, deadline) {
|
|
18476
|
+
try {
|
|
18477
|
+
const result = await runProviderCommandToDeadline(
|
|
18478
|
+
session,
|
|
18479
|
+
displayStackCleanupCommand(pending.markerPath, pending.markerToken),
|
|
18480
|
+
deadline
|
|
18481
|
+
);
|
|
18482
|
+
const output = execResultOutput(result);
|
|
18483
|
+
const status = /OPENGENI_DISPLAY_CLEANUP status=(stopped|absent|alive|invalid)/u.exec(
|
|
18484
|
+
output
|
|
18485
|
+
)?.[1];
|
|
18486
|
+
return status === "stopped";
|
|
18487
|
+
} catch {
|
|
18488
|
+
return false;
|
|
18489
|
+
}
|
|
18490
|
+
}
|
|
18491
|
+
async function reconcilePendingCleanup(session, state, deadline) {
|
|
18492
|
+
const entries = [...state.pendingCleanup.values()];
|
|
18493
|
+
if (entries.length === 0) {
|
|
18494
|
+
state.physicallyQuiescent = true;
|
|
18495
|
+
return true;
|
|
18496
|
+
}
|
|
18497
|
+
const results = await Promise.all(
|
|
18498
|
+
entries.map(async (pending) => ({
|
|
18499
|
+
pending,
|
|
18500
|
+
stopped: await cleanupDisplayStackProcess(session, pending, deadline)
|
|
18501
|
+
}))
|
|
18502
|
+
);
|
|
18503
|
+
for (const { pending, stopped } of results) {
|
|
18504
|
+
if (stopped) state.pendingCleanup.delete(pending.markerToken);
|
|
18505
|
+
}
|
|
18506
|
+
state.physicallyQuiescent = state.pendingCleanup.size === 0;
|
|
18507
|
+
return state.physicallyQuiescent;
|
|
18508
|
+
}
|
|
18509
|
+
function requestId() {
|
|
18510
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
18511
|
+
}
|
|
18512
|
+
function telemetryReporter(options, started, now = monotonicNow) {
|
|
18513
|
+
const id = requestId();
|
|
18514
|
+
const context = options.telemetryContext;
|
|
18515
|
+
const sink = options.onTelemetry ?? (context ? (event) => console.info(`[display-stack] ${JSON.stringify(event)}`) : void 0);
|
|
18516
|
+
const emittedSandboxStages = /* @__PURE__ */ new Set();
|
|
18517
|
+
const emit = (stage, status, fields = {}) => {
|
|
18518
|
+
if (!sink) return;
|
|
18519
|
+
const event = {
|
|
18520
|
+
type: "display_stack.stage",
|
|
18521
|
+
requestId: id,
|
|
18522
|
+
stage,
|
|
18523
|
+
status,
|
|
18524
|
+
elapsedMs: fields.elapsedMs ?? now() - started,
|
|
18525
|
+
source: fields.source ?? "host",
|
|
18526
|
+
classification: fields.classification ?? "unknown",
|
|
18527
|
+
callerKind: context?.callerKind ?? "unknown",
|
|
18528
|
+
...context?.sandboxId ? { sandboxId: context.sandboxId } : {},
|
|
18529
|
+
...context?.leaseEpoch !== void 0 ? { leaseEpoch: context.leaseEpoch } : {},
|
|
18530
|
+
...fields.providerSessionId !== void 0 ? { providerSessionId: fields.providerSessionId } : {}
|
|
18531
|
+
};
|
|
18532
|
+
try {
|
|
18533
|
+
void Promise.resolve(sink(event)).catch(() => void 0);
|
|
18534
|
+
} catch {
|
|
18535
|
+
}
|
|
18536
|
+
};
|
|
18537
|
+
const emitSandboxStages = (output) => {
|
|
18538
|
+
for (const match of output.matchAll(
|
|
18539
|
+
/OPENGENI_(?:DISPLAY|DESKTOP)_STAGE stage=([a-z0-9_]+) elapsed_ms=(\d+)(?: classification=(cold|already_ready|contention|unknown))?/gu
|
|
18540
|
+
)) {
|
|
18541
|
+
const [line, stage, elapsed, classification] = match;
|
|
18542
|
+
if (!stage || !elapsed || emittedSandboxStages.has(line)) continue;
|
|
18543
|
+
emittedSandboxStages.add(line);
|
|
18544
|
+
emit(stage, "completed", {
|
|
18545
|
+
source: "sandbox",
|
|
18546
|
+
elapsedMs: Number(elapsed),
|
|
18547
|
+
classification: classification ?? "unknown"
|
|
18548
|
+
});
|
|
18549
|
+
}
|
|
18550
|
+
};
|
|
18551
|
+
return { emit, emitSandboxStages };
|
|
18552
|
+
}
|
|
17782
18553
|
function inferExitFromOutput(output) {
|
|
18554
|
+
if (/OPENGENI_DISPLAY_TIMEOUT/.test(output)) {
|
|
18555
|
+
return 124;
|
|
18556
|
+
}
|
|
18557
|
+
if (/OPENGENI_DESKTOP_LOCK_TIMEOUT/.test(output)) {
|
|
18558
|
+
return 15;
|
|
18559
|
+
}
|
|
17783
18560
|
if (/OPENGENI_DESKTOP_NOT_PAINTING/.test(output)) {
|
|
17784
18561
|
return 14;
|
|
17785
18562
|
}
|
|
@@ -17807,15 +18584,163 @@ async function ensureDisplayStack(session, options = {}) {
|
|
|
17807
18584
|
const geometry = options.geometry ?? DEFAULT_DESKTOP_GEOMETRY;
|
|
17808
18585
|
const port = options.port ?? DESKTOP_STREAM_PORT4;
|
|
17809
18586
|
const timeoutMs = options.timeoutMs ?? DISPLAY_STACK_TIMEOUT_MS;
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
const
|
|
17814
|
-
|
|
17815
|
-
|
|
18587
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
18588
|
+
throw new RangeError("ensureDisplayStack timeoutMs must be a positive finite number");
|
|
18589
|
+
}
|
|
18590
|
+
const started = monotonicNow();
|
|
18591
|
+
const deadline = started + timeoutMs;
|
|
18592
|
+
const telemetry = telemetryReporter(options, started, monotonicNow);
|
|
18593
|
+
const operationToken = requestId();
|
|
18594
|
+
const markerPath = `/tmp/opengeni-desktop/display-stack-${operationToken}.pid`;
|
|
18595
|
+
const cmd = boundedDisplayStackCommand(
|
|
18596
|
+
buildDisplayStackScript({ geometry, port }),
|
|
18597
|
+
timeoutMs,
|
|
18598
|
+
markerPath,
|
|
18599
|
+
operationToken
|
|
18600
|
+
);
|
|
18601
|
+
const outputParts = [];
|
|
18602
|
+
const state = operationState(session, options);
|
|
18603
|
+
let release;
|
|
18604
|
+
if (state) {
|
|
18605
|
+
try {
|
|
18606
|
+
release = await acquireOperation(state, deadline);
|
|
18607
|
+
} catch (error) {
|
|
18608
|
+
if (error instanceof DisplayStackWallTimeoutError || error instanceof DisplayStackOperationFencedError) {
|
|
18609
|
+
throw new DisplayStackError(
|
|
18610
|
+
124,
|
|
18611
|
+
"OPENGENI_DISPLAY_TIMEOUT display operation admission was fenced"
|
|
18612
|
+
);
|
|
18613
|
+
}
|
|
18614
|
+
throw error;
|
|
18615
|
+
}
|
|
18616
|
+
}
|
|
18617
|
+
try {
|
|
18618
|
+
if (state && !state.physicallyQuiescent) {
|
|
18619
|
+
const cleanupDeadline = monotonicNow() + DISPLAY_STACK_CLEANUP_CALL_MS;
|
|
18620
|
+
const quiesced = await reconcilePendingCleanup(s, state, cleanupDeadline);
|
|
18621
|
+
if (!quiesced) {
|
|
18622
|
+
throw new DisplayStackError(
|
|
18623
|
+
124,
|
|
18624
|
+
"OPENGENI_DISPLAY_TIMEOUT physical cleanup is still pending; retry is fenced"
|
|
18625
|
+
);
|
|
18626
|
+
}
|
|
18627
|
+
}
|
|
18628
|
+
const epoch = state?.epoch ?? 0;
|
|
18629
|
+
const abortController = new AbortController();
|
|
18630
|
+
let activeProviderSessionId = null;
|
|
18631
|
+
telemetry.emit("request_entry", "started");
|
|
18632
|
+
telemetry.emit("exec_issued", "started");
|
|
18633
|
+
try {
|
|
18634
|
+
const initialYieldMs = Math.max(1, Math.min(DISPLAY_STACK_PROVIDER_YIELD_MS, timeoutMs - 1));
|
|
18635
|
+
const initialExec = trackProviderCall(
|
|
18636
|
+
state,
|
|
18637
|
+
Promise.resolve(
|
|
18638
|
+
typeof s.exec === "function" ? s.exec({
|
|
18639
|
+
cmd,
|
|
18640
|
+
yieldTimeMs: initialYieldMs,
|
|
18641
|
+
maxOutputTokens: 2e4,
|
|
18642
|
+
signal: abortController.signal
|
|
18643
|
+
}) : s.execCommand({
|
|
18644
|
+
cmd,
|
|
18645
|
+
yieldTimeMs: initialYieldMs,
|
|
18646
|
+
maxOutputTokens: 2e4,
|
|
18647
|
+
signal: abortController.signal
|
|
18648
|
+
})
|
|
18649
|
+
)
|
|
18650
|
+
);
|
|
18651
|
+
let result = await beforeDeadline(
|
|
18652
|
+
initialExec,
|
|
18653
|
+
deadline,
|
|
18654
|
+
monotonicNow
|
|
18655
|
+
);
|
|
18656
|
+
assertCurrentOperation(state, epoch);
|
|
18657
|
+
let chunk = execResultOutput(result);
|
|
18658
|
+
outputParts.push(chunk);
|
|
18659
|
+
telemetry.emitSandboxStages(chunk);
|
|
18660
|
+
telemetry.emit("provider_first_result", "completed");
|
|
18661
|
+
let providerSessionId = execResultSessionId(result);
|
|
18662
|
+
activeProviderSessionId = providerSessionId;
|
|
18663
|
+
while (providerSessionId !== null) {
|
|
18664
|
+
telemetry.emit("provider_yield", "waiting", { providerSessionId });
|
|
18665
|
+
if (typeof s.writeStdin !== "function") {
|
|
18666
|
+
throw new DisplayStackError(
|
|
18667
|
+
-1,
|
|
18668
|
+
`${outputParts.join("\n")}
|
|
18669
|
+
provider yielded process ${providerSessionId} but exposes no writeStdin poll surface`
|
|
18670
|
+
);
|
|
18671
|
+
}
|
|
18672
|
+
assertCurrentOperation(state, epoch);
|
|
18673
|
+
const remainingMs = deadline - monotonicNow();
|
|
18674
|
+
if (remainingMs <= 1) throw new DisplayStackWallTimeoutError();
|
|
18675
|
+
const pollYieldMs = Math.max(1, Math.min(DISPLAY_STACK_PROVIDER_POLL_MS, remainingMs - 1));
|
|
18676
|
+
result = await beforeDeadline(
|
|
18677
|
+
trackProviderCall(
|
|
18678
|
+
state,
|
|
18679
|
+
s.writeStdin({
|
|
18680
|
+
sessionId: providerSessionId,
|
|
18681
|
+
chars: "",
|
|
18682
|
+
yieldTimeMs: pollYieldMs,
|
|
18683
|
+
maxOutputTokens: 2e4,
|
|
18684
|
+
signal: abortController.signal
|
|
18685
|
+
})
|
|
18686
|
+
),
|
|
18687
|
+
deadline,
|
|
18688
|
+
monotonicNow
|
|
18689
|
+
);
|
|
18690
|
+
assertCurrentOperation(state, epoch);
|
|
18691
|
+
chunk = execResultOutput(result);
|
|
18692
|
+
outputParts.push(chunk);
|
|
18693
|
+
telemetry.emitSandboxStages(chunk);
|
|
18694
|
+
providerSessionId = execResultSessionId(result);
|
|
18695
|
+
activeProviderSessionId = providerSessionId;
|
|
18696
|
+
}
|
|
18697
|
+
const output = outputParts.join("\n");
|
|
18698
|
+
const exitCode = execResultExitCode(result) ?? inferExitFromOutput(output);
|
|
18699
|
+
telemetry.emit("process_complete", exitCode === 0 ? "completed" : "failed");
|
|
18700
|
+
if (exitCode !== 0) {
|
|
18701
|
+
throw new DisplayStackError(exitCode, output);
|
|
18702
|
+
}
|
|
18703
|
+
const marker = (output.match(/OPENGENI_DESKTOP_UP[^\n]*/) ?? [""])[0];
|
|
18704
|
+
telemetry.emit("readiness_complete", "completed");
|
|
18705
|
+
return { port, geometry, marker };
|
|
18706
|
+
} catch (error) {
|
|
18707
|
+
if (error instanceof DisplayStackWallTimeoutError || error instanceof DisplayStackOperationFencedError) {
|
|
18708
|
+
const pendingCleanup = {
|
|
18709
|
+
markerPath,
|
|
18710
|
+
markerToken: operationToken
|
|
18711
|
+
};
|
|
18712
|
+
fenceOperation(state, epoch, abortController, pendingCleanup);
|
|
18713
|
+
const cleanupDeadline = monotonicNow() + DISPLAY_STACK_CLEANUP_CALL_MS;
|
|
18714
|
+
telemetry.emit("process_cleanup", "started");
|
|
18715
|
+
await interruptProviderProcess(
|
|
18716
|
+
s,
|
|
18717
|
+
activeProviderSessionId,
|
|
18718
|
+
Math.min(cleanupDeadline, monotonicNow() + 50)
|
|
18719
|
+
);
|
|
18720
|
+
let quiesced = false;
|
|
18721
|
+
if (state) {
|
|
18722
|
+
quiesced = await reconcilePendingCleanup(s, state, cleanupDeadline);
|
|
18723
|
+
if (!quiesced) {
|
|
18724
|
+
await waitForFencedOperation(
|
|
18725
|
+
state,
|
|
18726
|
+
Math.min(cleanupDeadline, monotonicNow() + DISPLAY_STACK_DRAIN_GRACE_MS)
|
|
18727
|
+
);
|
|
18728
|
+
}
|
|
18729
|
+
} else {
|
|
18730
|
+
quiesced = await cleanupDisplayStackProcess(s, pendingCleanup, cleanupDeadline);
|
|
18731
|
+
}
|
|
18732
|
+
telemetry.emit("process_cleanup", quiesced ? "completed" : "failed");
|
|
18733
|
+
const output = `${outputParts.join("\n")}
|
|
18734
|
+
OPENGENI_DISPLAY_TIMEOUT wall_ms=${timeoutMs}`;
|
|
18735
|
+
telemetry.emit("wall_deadline", "failed");
|
|
18736
|
+
throw new DisplayStackError(124, output);
|
|
18737
|
+
}
|
|
18738
|
+
telemetry.emit("request_failed", "failed");
|
|
18739
|
+
throw error;
|
|
18740
|
+
}
|
|
18741
|
+
} finally {
|
|
18742
|
+
release?.();
|
|
17816
18743
|
}
|
|
17817
|
-
const marker = (output.match(/OPENGENI_DESKTOP_UP[^\n]*/) ?? [""])[0];
|
|
17818
|
-
return { port, geometry, marker };
|
|
17819
18744
|
}
|
|
17820
18745
|
async function tearDownDisplayStack(session) {
|
|
17821
18746
|
const s = session;
|
|
@@ -18235,7 +19160,7 @@ function runCredentialRoot(sessionId) {
|
|
|
18235
19160
|
function runCredentialPointerFile(sessionId) {
|
|
18236
19161
|
return `${runCredentialRoot(sessionId)}/current`;
|
|
18237
19162
|
}
|
|
18238
|
-
function
|
|
19163
|
+
function shellQuote4(value) {
|
|
18239
19164
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
18240
19165
|
}
|
|
18241
19166
|
function base64(value) {
|
|
@@ -18253,8 +19178,8 @@ function portableBase64DecodeCommand() {
|
|
|
18253
19178
|
].join("\n");
|
|
18254
19179
|
}
|
|
18255
19180
|
function pointerLockAcquireCommands(root) {
|
|
18256
|
-
const lockFile =
|
|
18257
|
-
const lockDirectory =
|
|
19181
|
+
const lockFile = shellQuote4(`${root}/.pointer.lock`);
|
|
19182
|
+
const lockDirectory = shellQuote4(`${root}/.pointer.lockdir`);
|
|
18258
19183
|
return [
|
|
18259
19184
|
`if command -v flock >/dev/null 2>&1; then`,
|
|
18260
19185
|
` exec 9>${lockFile}`,
|
|
@@ -18298,7 +19223,7 @@ async function runCredentialCommand(session, cmd, commandRunner) {
|
|
|
18298
19223
|
${cmd}
|
|
18299
19224
|
printf '\\n${COMMAND_OK_MARKER}\\n'`;
|
|
18300
19225
|
const args = {
|
|
18301
|
-
cmd: `env -u BASH_ENV bash --noprofile --norc -c ${
|
|
19226
|
+
cmd: `env -u BASH_ENV bash --noprofile --norc -c ${shellQuote4(marked)}`,
|
|
18302
19227
|
yieldTimeMs: 12e4,
|
|
18303
19228
|
maxOutputTokens: 4e3
|
|
18304
19229
|
};
|
|
@@ -18308,19 +19233,19 @@ printf '\\n${COMMAND_OK_MARKER}\\n'`;
|
|
|
18308
19233
|
}
|
|
18309
19234
|
}
|
|
18310
19235
|
async function writeCredentialFile(session, path, content, commandRunner) {
|
|
18311
|
-
await runCredentialCommand(session, `: > ${
|
|
19236
|
+
await runCredentialCommand(session, `: > ${shellQuote4(path)}`, commandRunner);
|
|
18312
19237
|
const encoded = base64(content);
|
|
18313
19238
|
for (let offset = 0; offset < encoded.length; offset += WRITE_CHUNK_BYTES) {
|
|
18314
19239
|
const chunk = encoded.slice(offset, offset + WRITE_CHUNK_BYTES);
|
|
18315
19240
|
await runCredentialCommand(
|
|
18316
19241
|
session,
|
|
18317
|
-
`printf %s ${
|
|
19242
|
+
`printf %s ${shellQuote4(chunk)} | ${portableBase64DecodeCommand()} >> ${shellQuote4(path)}`,
|
|
18318
19243
|
commandRunner
|
|
18319
19244
|
);
|
|
18320
19245
|
}
|
|
18321
19246
|
await runCredentialCommand(
|
|
18322
19247
|
session,
|
|
18323
|
-
`[ "$(wc -c < ${
|
|
19248
|
+
`[ "$(wc -c < ${shellQuote4(path)} | tr -d '[:space:]')" = ${shellQuote4(String(byteLength(content)))} ]`,
|
|
18324
19249
|
commandRunner
|
|
18325
19250
|
);
|
|
18326
19251
|
}
|
|
@@ -18345,19 +19270,19 @@ async function materializeRunCredentials(session, material, options) {
|
|
|
18345
19270
|
session,
|
|
18346
19271
|
[
|
|
18347
19272
|
"umask 077",
|
|
18348
|
-
`mkdir -p -- ${
|
|
18349
|
-
`chmod 0700 -- ${
|
|
18350
|
-
`rm -rf -- ${
|
|
18351
|
-
`mkdir -p -- ${
|
|
19273
|
+
`mkdir -p -- ${shellQuote4(`${root}/versions`)}`,
|
|
19274
|
+
`chmod 0700 -- ${shellQuote4(root)} ${shellQuote4(`${root}/versions`)}`,
|
|
19275
|
+
`rm -rf -- ${shellQuote4(stage)}`,
|
|
19276
|
+
`mkdir -p -- ${shellQuote4(`${stage}/files`)}`
|
|
18352
19277
|
].join("\n"),
|
|
18353
19278
|
options.commandRunner
|
|
18354
19279
|
);
|
|
18355
19280
|
const environmentLines = [
|
|
18356
19281
|
...Object.entries(material.environment).map(
|
|
18357
|
-
([name, value]) => `export ${name}=${
|
|
19282
|
+
([name, value]) => `export ${name}=${shellQuote4(value)}`
|
|
18358
19283
|
),
|
|
18359
19284
|
...Object.entries(material.fileEnvironment).map(
|
|
18360
|
-
([name, path]) => `export ${name}=${
|
|
19285
|
+
([name, path]) => `export ${name}=${shellQuote4(`${version}/files/${path}`)}`
|
|
18361
19286
|
)
|
|
18362
19287
|
];
|
|
18363
19288
|
await writeCredentialFile(
|
|
@@ -18369,17 +19294,17 @@ async function materializeRunCredentials(session, material, options) {
|
|
|
18369
19294
|
);
|
|
18370
19295
|
await runCredentialCommand(
|
|
18371
19296
|
session,
|
|
18372
|
-
`chmod 0600 -- ${
|
|
19297
|
+
`chmod 0600 -- ${shellQuote4(`${stage}/env`)}`,
|
|
18373
19298
|
options.commandRunner
|
|
18374
19299
|
);
|
|
18375
19300
|
for (const file of material.files) {
|
|
18376
19301
|
const target = `${stage}/files/${file.path}`;
|
|
18377
19302
|
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
18378
|
-
await runCredentialCommand(session, `mkdir -p -- ${
|
|
19303
|
+
await runCredentialCommand(session, `mkdir -p -- ${shellQuote4(parent)}`, options.commandRunner);
|
|
18379
19304
|
await writeCredentialFile(session, target, file.content, options.commandRunner);
|
|
18380
19305
|
await runCredentialCommand(
|
|
18381
19306
|
session,
|
|
18382
|
-
`chmod ${
|
|
19307
|
+
`chmod ${shellQuote4(file.mode ?? "0600")} -- ${shellQuote4(target)}`,
|
|
18383
19308
|
options.commandRunner
|
|
18384
19309
|
);
|
|
18385
19310
|
}
|
|
@@ -18387,19 +19312,19 @@ async function materializeRunCredentials(session, material, options) {
|
|
|
18387
19312
|
session,
|
|
18388
19313
|
[
|
|
18389
19314
|
...pointerLockAcquireCommands(root),
|
|
18390
|
-
`previous=$(cat -- ${
|
|
18391
|
-
`mv -- ${
|
|
18392
|
-
`printf '%s\\n' ${
|
|
18393
|
-
`mv -f -- ${
|
|
19315
|
+
`previous=$(cat -- ${shellQuote4(`${root}/current`)} 2>/dev/null || :)`,
|
|
19316
|
+
`mv -- ${shellQuote4(stage)} ${shellQuote4(version)}`,
|
|
19317
|
+
`printf '%s\\n' ${shellQuote4(versionName)} > ${shellQuote4(`${root}/.next`)}`,
|
|
19318
|
+
`mv -f -- ${shellQuote4(`${root}/.next`)} ${shellQuote4(`${root}/current`)}`,
|
|
18394
19319
|
...options.prunePreviousGenerations ? [
|
|
18395
|
-
`find ${
|
|
19320
|
+
`find ${shellQuote4(`${root}/versions`)} -mindepth 1 -maxdepth 1 ! -name ${shellQuote4(versionName)} -exec rm -rf -- {} +`
|
|
18396
19321
|
] : options.pruneOtherAttempts ? [
|
|
18397
|
-
`find ${
|
|
19322
|
+
`find ${shellQuote4(`${root}/versions`)} -mindepth 1 -maxdepth 1 ! -name ${shellQuote4(`${attemptId}-${options.executionGeneration}-*`)} -exec rm -rf -- {} +`
|
|
18398
19323
|
] : options.pruneSupersededGenerations ? [
|
|
18399
|
-
`for candidate in ${
|
|
19324
|
+
`for candidate in ${shellQuote4(`${root}/versions`)}/${prefix}*; do`,
|
|
18400
19325
|
` [ -e "$candidate" ] || continue`,
|
|
18401
19326
|
` name=\${candidate##*/}`,
|
|
18402
|
-
` [ "$name" = ${
|
|
19327
|
+
` [ "$name" = ${shellQuote4(versionName)} ] || [ "$name" = "$previous" ] || rm -rf -- "$candidate"`,
|
|
18403
19328
|
"done"
|
|
18404
19329
|
] : [],
|
|
18405
19330
|
...pointerLockReleaseCommands()
|
|
@@ -18412,10 +19337,10 @@ async function clearRunCredentials(session, sessionId, commandRunner) {
|
|
|
18412
19337
|
await runCredentialCommand(
|
|
18413
19338
|
session,
|
|
18414
19339
|
[
|
|
18415
|
-
`if [ -d ${
|
|
19340
|
+
`if [ -d ${shellQuote4(root)} ]; then`,
|
|
18416
19341
|
...pointerLockAcquireCommands(root).map((line) => ` ${line}`),
|
|
18417
|
-
` rm -f -- ${
|
|
18418
|
-
` rm -rf -- ${
|
|
19342
|
+
` rm -f -- ${shellQuote4(`${root}/current`)} ${shellQuote4(`${root}/.next`)}`,
|
|
19343
|
+
` rm -rf -- ${shellQuote4(`${root}/versions`)}`,
|
|
18419
19344
|
...pointerLockReleaseCommands().map((line) => ` ${line}`),
|
|
18420
19345
|
"fi"
|
|
18421
19346
|
].join("\n"),
|
|
@@ -18432,15 +19357,15 @@ async function clearRunCredentialsForAttempt(session, options, commandRunner) {
|
|
|
18432
19357
|
await runCredentialCommand(
|
|
18433
19358
|
session,
|
|
18434
19359
|
[
|
|
18435
|
-
`if [ -d ${
|
|
19360
|
+
`if [ -d ${shellQuote4(root)} ]; then`,
|
|
18436
19361
|
...pointerLockAcquireCommands(root).map((line) => ` ${line}`),
|
|
18437
|
-
` if [ -r ${
|
|
18438
|
-
` current=$(cat -- ${
|
|
18439
|
-
` case "$current" in ${
|
|
19362
|
+
` if [ -r ${shellQuote4(`${root}/current`)} ]; then`,
|
|
19363
|
+
` current=$(cat -- ${shellQuote4(`${root}/current`)} 2>/dev/null || :)`,
|
|
19364
|
+
` case "$current" in ${shellQuote4(prefix)}*) rm -f -- ${shellQuote4(`${root}/current`)} ;; esac`,
|
|
18440
19365
|
" fi",
|
|
18441
|
-
` if [ -d ${
|
|
18442
|
-
` find ${
|
|
18443
|
-
` find ${
|
|
19366
|
+
` if [ -d ${shellQuote4(`${root}/versions`)} ]; then`,
|
|
19367
|
+
` find ${shellQuote4(`${root}/versions`)} -mindepth 1 -maxdepth 1 -name ${shellQuote4(`${prefix}*`)} -exec rm -rf -- {} +`,
|
|
19368
|
+
` find ${shellQuote4(`${root}/versions`)} -mindepth 1 -maxdepth 1 -name ${shellQuote4(`.stage-${prefix}*`)} -exec rm -rf -- {} +`,
|
|
18444
19369
|
" fi",
|
|
18445
19370
|
...pointerLockReleaseCommands().map((line) => ` ${line}`),
|
|
18446
19371
|
"fi"
|
|
@@ -18452,12 +19377,12 @@ function withRunCredentialEnvironment(cmd, sessionId) {
|
|
|
18452
19377
|
const root = runCredentialRoot(sessionId);
|
|
18453
19378
|
const pointerFile = runCredentialPointerFile(sessionId);
|
|
18454
19379
|
return [
|
|
18455
|
-
`if [ -r ${
|
|
18456
|
-
` _opengeni_credential_version=$(cat -- ${
|
|
19380
|
+
`if [ -r ${shellQuote4(pointerFile)} ]; then`,
|
|
19381
|
+
` _opengeni_credential_version=$(cat -- ${shellQuote4(pointerFile)} 2>/dev/null || :)`,
|
|
18457
19382
|
' case "$_opengeni_credential_version" in',
|
|
18458
19383
|
" ''|*[!A-Za-z0-9_-]*) ;;",
|
|
18459
19384
|
" *)",
|
|
18460
|
-
` _opengeni_credential_env=${
|
|
19385
|
+
` _opengeni_credential_env=${shellQuote4(`${root}/versions`)}/"$_opengeni_credential_version/env"`,
|
|
18461
19386
|
' if [ -r "$_opengeni_credential_env" ]; then',
|
|
18462
19387
|
" set -a",
|
|
18463
19388
|
' . "$_opengeni_credential_env"',
|
|
@@ -18525,7 +19450,7 @@ function withRunCredentialsClient(client, sessionId, onSessionReady) {
|
|
|
18525
19450
|
}
|
|
18526
19451
|
|
|
18527
19452
|
// src/sandbox/toolspace-token.ts
|
|
18528
|
-
import { createHash as
|
|
19453
|
+
import { createHash as createHash5 } from "crypto";
|
|
18529
19454
|
import { posix as posixPath } from "path";
|
|
18530
19455
|
var MAX_TOOLSPACE_SESSION_ID_BYTES = 512;
|
|
18531
19456
|
var ToolspaceTokenPathError = class extends Error {
|
|
@@ -18534,7 +19459,7 @@ var ToolspaceTokenPathError = class extends Error {
|
|
|
18534
19459
|
this.name = "ToolspaceTokenPathError";
|
|
18535
19460
|
}
|
|
18536
19461
|
};
|
|
18537
|
-
function
|
|
19462
|
+
function shellQuote5(value) {
|
|
18538
19463
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
18539
19464
|
}
|
|
18540
19465
|
function assertToolspaceSessionId(sessionId) {
|
|
@@ -18548,7 +19473,7 @@ function toolspaceTokenFileForSession(manifestTokenFile, sessionId) {
|
|
|
18548
19473
|
throw new ToolspaceTokenPathError("Toolspace manifest token file must be absolute");
|
|
18549
19474
|
}
|
|
18550
19475
|
const normalizedManifestFile = posixPath.normalize(manifestTokenFile);
|
|
18551
|
-
const digest =
|
|
19476
|
+
const digest = createHash5("sha256").update(assertToolspaceSessionId(sessionId)).digest("hex");
|
|
18552
19477
|
return posixPath.join(posixPath.dirname(normalizedManifestFile), "toolspace-tokens", digest);
|
|
18553
19478
|
}
|
|
18554
19479
|
function toolspaceTokenFileFromEnvironment(environment, sessionId) {
|
|
@@ -18559,7 +19484,7 @@ function withToolspaceTokenEnvironment(cmd, tokenFile) {
|
|
|
18559
19484
|
if (!tokenFile || tokenFile.includes("\0") || !posixPath.isAbsolute(tokenFile)) {
|
|
18560
19485
|
throw new ToolspaceTokenPathError("Toolspace token file must be absolute");
|
|
18561
19486
|
}
|
|
18562
|
-
return [`export OPENGENI_TOOLSPACE_TOKEN_FILE=${
|
|
19487
|
+
return [`export OPENGENI_TOOLSPACE_TOKEN_FILE=${shellQuote5(tokenFile)}`, cmd].join("\n");
|
|
18563
19488
|
}
|
|
18564
19489
|
function withToolspaceTokenSession(session, tokenFile) {
|
|
18565
19490
|
return new Proxy(session, {
|
|
@@ -18828,6 +19753,804 @@ function recordingStorageKey(workspaceId, sessionId, recordingId, codec) {
|
|
|
18828
19753
|
return `recordings/${workspaceId}/${sessionId}/${recordingId}.${extForCodec(codec)}`;
|
|
18829
19754
|
}
|
|
18830
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);
|
|
20546
|
+
}
|
|
20547
|
+
function wrapCapabilityToolsForTurnCancellation(capability, controller) {
|
|
20548
|
+
const originalTools = capability.tools;
|
|
20549
|
+
capability.tools = function() {
|
|
20550
|
+
return controller.wrapTools(originalTools.call(this), this._session);
|
|
20551
|
+
};
|
|
20552
|
+
}
|
|
20553
|
+
|
|
18831
20554
|
// src/sandbox/channel-a.ts
|
|
18832
20555
|
var ChannelAValidationError = class extends Error {
|
|
18833
20556
|
constructor(message) {
|
|
@@ -18953,11 +20676,11 @@ var SandboxChannelAService = class {
|
|
|
18953
20676
|
const depthArg = Math.max(1, req.depth);
|
|
18954
20677
|
const maxCommandEntries = req.maxEntries + 1;
|
|
18955
20678
|
const gnuPrint = `-printf '%y\\t%s\\t%T@\\t%m\\t%p\\0'`;
|
|
18956
|
-
const gnuSelector = pruneNames.length ? `\\( -type d \\( ${pruneNames.map((name) => `-name ${
|
|
20679
|
+
const gnuSelector = pruneNames.length ? `\\( -type d \\( ${pruneNames.map((name) => `-name ${shellQuote6(name)}`).join(" -o ")} \\) ${gnuPrint} -prune \\) -o ${gnuPrint}` : gnuPrint;
|
|
18957
20680
|
const gnuVisibleSelector = req.includeHidden ? gnuSelector : `\\( -path '*/.*' -prune \\) -o \\( ${gnuSelector} \\)`;
|
|
18958
20681
|
const gnuFind = `find ${findRoot} -mindepth 1 -maxdepth ${depthArg} \\( ${gnuVisibleSelector} \\) 2>/dev/null`;
|
|
18959
20682
|
const portableHiddenGuard = req.includeHidden ? "" : `if [[ "$base" == .* ]]; then continue; fi;`;
|
|
18960
|
-
const portablePruneCase = pruneNames.length ? `case "$base" in ${pruneNames.map(
|
|
20683
|
+
const portablePruneCase = pruneNames.length ? `case "$base" in ${pruneNames.map(shellQuote6).join("|")}) pruned=1 ;; *) pruned=0 ;; esac;` : "pruned=0;";
|
|
18961
20684
|
const portableWalk = [
|
|
18962
20685
|
"shopt -s nullglob dotglob; count=0; stop=0;",
|
|
18963
20686
|
'walk() { local dir="$1" level="$2" p base t size mtime mode pruned;',
|
|
@@ -18986,12 +20709,12 @@ var SandboxChannelAService = class {
|
|
|
18986
20709
|
`if [ "$records" -ge ${req.maxEntries} ] || [ $((bytes + record_bytes + 1)) -gt ${FS_LIST_MAX_OUTPUT_BYTES} ]; then truncated=1; break; fi;`,
|
|
18987
20710
|
`printf '%s\\0' "$record"; bytes=$((bytes + record_bytes + 1)); records=$((records + 1));`,
|
|
18988
20711
|
"done;",
|
|
18989
|
-
`if [ "$truncated" -eq 1 ]; then printf '%s\\0' ${
|
|
20712
|
+
`if [ "$truncated" -eq 1 ]; then printf '%s\\0' ${shellQuote6(FS_LIST_TRUNCATED_MARKER)}; fi`
|
|
18990
20713
|
].join(" ");
|
|
18991
20714
|
const findCommand = [
|
|
18992
20715
|
`{ ${rawFindCommand}; } | { ${boundedOutput}; };`,
|
|
18993
20716
|
"producer_status=${PIPESTATUS[0]};",
|
|
18994
|
-
`if [ "$producer_status" -ne 0 ] && [ "$producer_status" -ne 141 ]; then printf '%s\\0' ${
|
|
20717
|
+
`if [ "$producer_status" -ne 0 ] && [ "$producer_status" -ne 141 ]; then printf '%s\\0' ${shellQuote6(FS_LIST_TRUNCATED_MARKER)}; fi`
|
|
18995
20718
|
].join(" ");
|
|
18996
20719
|
const { stdout, exitCode } = await this.runInConfinedDirectory(root, {
|
|
18997
20720
|
cmd: internalBashCommand(findCommand),
|
|
@@ -19084,11 +20807,11 @@ var SandboxChannelAService = class {
|
|
|
19084
20807
|
async assertConfinedExistingDirectory(path) {
|
|
19085
20808
|
const root = this.workspaceRoot || ".";
|
|
19086
20809
|
const abs = this.joinRoot(path);
|
|
19087
|
-
const rejectLink = path ? `test ! -L ${
|
|
20810
|
+
const rejectLink = path ? `test ! -L ${shellQuote6(abs)} || { printf '__OPENGENI_FS_SYMLINK__'; exit 68; }` : ":";
|
|
19088
20811
|
const script = [
|
|
19089
|
-
`root=$(realpath -e -- ${
|
|
20812
|
+
`root=$(realpath -e -- ${shellQuote6(root)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19090
20813
|
rejectLink,
|
|
19091
|
-
`target=$(realpath -e -- ${
|
|
20814
|
+
`target=$(realpath -e -- ${shellQuote6(abs)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19092
20815
|
`case "$target" in "$root"|"$root"/*) ;; *) printf '__OPENGENI_FS_ESCAPE__'; exit 67 ;; esac`,
|
|
19093
20816
|
`test -d "$target" || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19094
20817
|
`printf '__OPENGENI_FS_CONFINED_OK__'`
|
|
@@ -19099,13 +20822,13 @@ var SandboxChannelAService = class {
|
|
|
19099
20822
|
async assertConfinedMutationParent(path, options) {
|
|
19100
20823
|
const root = this.workspaceRoot || ".";
|
|
19101
20824
|
const abs = this.joinRoot(path);
|
|
19102
|
-
const rejectLink = options.rejectFinalSymlink ? `test ! -L ${
|
|
20825
|
+
const rejectLink = options.rejectFinalSymlink ? `test ! -L ${shellQuote6(abs)} || { printf '__OPENGENI_FS_SYMLINK__'; exit 68; }` : ":";
|
|
19103
20826
|
const locateParent = options.allowMissingParents ? 'probe="$parent"; while [ ! -e "$probe" ] && [ ! -L "$probe" ]; do next=$(dirname -- "$probe"); [ "$next" != "$probe" ] || break; probe="$next"; done' : 'probe="$parent"';
|
|
19104
20827
|
const requireParent = options.allowMissingParents ? ":" : 'test -d "$parent" || { printf "__OPENGENI_FS_NOT_FOUND__"; exit 66; }';
|
|
19105
20828
|
const script = [
|
|
19106
|
-
`root=$(realpath -e -- ${
|
|
20829
|
+
`root=$(realpath -e -- ${shellQuote6(root)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19107
20830
|
rejectLink,
|
|
19108
|
-
`parent=$(dirname -- ${
|
|
20831
|
+
`parent=$(dirname -- ${shellQuote6(abs)})`,
|
|
19109
20832
|
locateParent,
|
|
19110
20833
|
`target=$(realpath -e -- "$probe") || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19111
20834
|
`case "$target" in "$root"|"$root"/*) ;; *) printf '__OPENGENI_FS_ESCAPE__'; exit 67 ;; esac`,
|
|
@@ -19142,8 +20865,8 @@ var SandboxChannelAService = class {
|
|
|
19142
20865
|
const root = this.workspaceRoot || ".";
|
|
19143
20866
|
const abs = this.joinRoot(path);
|
|
19144
20867
|
const script = [
|
|
19145
|
-
`root=$(realpath -e -- ${
|
|
19146
|
-
`exec 3<${
|
|
20868
|
+
`root=$(realpath -e -- ${shellQuote6(root)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
20869
|
+
`exec 3<${shellQuote6(abs)} || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19147
20870
|
`target=$(readlink -f -- "/proc/$$/fd/3") || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19148
20871
|
`case "$target" in "$root"|"$root"/*) ;; *) printf '__OPENGENI_FS_ESCAPE__'; exit 67 ;; esac`,
|
|
19149
20872
|
`test -f "$target" || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
@@ -19212,7 +20935,7 @@ var SandboxChannelAService = class {
|
|
|
19212
20935
|
});
|
|
19213
20936
|
if (!req.overwrite) {
|
|
19214
20937
|
const { exitCode: exitCode2 } = await this.run({
|
|
19215
|
-
cmd: `test -e ${
|
|
20938
|
+
cmd: `test -e ${shellQuote6(abs)} || test -L ${shellQuote6(abs)}`
|
|
19216
20939
|
});
|
|
19217
20940
|
if (exitCode2 === 0) {
|
|
19218
20941
|
throw new ChannelAConflictError(`path exists and overwrite is false: ${path}`);
|
|
@@ -19220,7 +20943,7 @@ var SandboxChannelAService = class {
|
|
|
19220
20943
|
}
|
|
19221
20944
|
if (req.createParents) {
|
|
19222
20945
|
const dir = dirnameAbs(abs);
|
|
19223
|
-
if (dir) await this.run({ cmd: `mkdir -p ${
|
|
20946
|
+
if (dir) await this.run({ cmd: `mkdir -p ${shellQuote6(dir)}` });
|
|
19224
20947
|
await this.assertConfinedMutationParent(path, {
|
|
19225
20948
|
allowMissingParents: false,
|
|
19226
20949
|
rejectFinalSymlink: true
|
|
@@ -19228,7 +20951,7 @@ var SandboxChannelAService = class {
|
|
|
19228
20951
|
}
|
|
19229
20952
|
const b64 = bytes.toString("base64");
|
|
19230
20953
|
const { exitCode, stderr } = await this.run({
|
|
19231
|
-
cmd: `printf %s ${
|
|
20954
|
+
cmd: `printf %s ${shellQuote6(b64)} | base64 -d > ${shellQuote6(abs)}`
|
|
19232
20955
|
});
|
|
19233
20956
|
if (exitCode !== null && exitCode !== 0) {
|
|
19234
20957
|
if (req.encoding !== "base64" && this.session.createEditor) {
|
|
@@ -19269,7 +20992,7 @@ var SandboxChannelAService = class {
|
|
|
19269
20992
|
rejectFinalSymlink: false
|
|
19270
20993
|
});
|
|
19271
20994
|
const flag = req.recursive ? "-rf" : "-f";
|
|
19272
|
-
const { exitCode, stderr } = await this.run({ cmd: `rm ${flag} ${
|
|
20995
|
+
const { exitCode, stderr } = await this.run({ cmd: `rm ${flag} ${shellQuote6(abs)}` });
|
|
19273
20996
|
if (exitCode !== null && exitCode !== 0) {
|
|
19274
20997
|
throw new ChannelAValidationError(
|
|
19275
20998
|
`failed to delete ${path}: ${stderr || `exit ${exitCode}`}`
|
|
@@ -19294,7 +21017,7 @@ var SandboxChannelAService = class {
|
|
|
19294
21017
|
});
|
|
19295
21018
|
if (!req.overwrite) {
|
|
19296
21019
|
const { exitCode: exitCode2 } = await this.run({
|
|
19297
|
-
cmd: `test -e ${
|
|
21020
|
+
cmd: `test -e ${shellQuote6(newAbs)} || test -L ${shellQuote6(newAbs)}`
|
|
19298
21021
|
});
|
|
19299
21022
|
if (exitCode2 === 0) {
|
|
19300
21023
|
throw new ChannelAConflictError(`destination exists and overwrite is false: ${newPath}`);
|
|
@@ -19302,7 +21025,7 @@ var SandboxChannelAService = class {
|
|
|
19302
21025
|
}
|
|
19303
21026
|
if (req.createParents) {
|
|
19304
21027
|
const dir = dirnameAbs(newAbs);
|
|
19305
|
-
if (dir) await this.run({ cmd: `mkdir -p ${
|
|
21028
|
+
if (dir) await this.run({ cmd: `mkdir -p ${shellQuote6(dir)}` });
|
|
19306
21029
|
await this.assertConfinedMutationParent(newPath, {
|
|
19307
21030
|
allowMissingParents: false,
|
|
19308
21031
|
rejectFinalSymlink: true
|
|
@@ -19310,7 +21033,7 @@ var SandboxChannelAService = class {
|
|
|
19310
21033
|
}
|
|
19311
21034
|
const flag = req.overwrite ? "-f " : "";
|
|
19312
21035
|
const { exitCode, stderr } = await this.run({
|
|
19313
|
-
cmd: `mv ${flag}${
|
|
21036
|
+
cmd: `mv ${flag}${shellQuote6(abs)} ${shellQuote6(newAbs)}`
|
|
19314
21037
|
});
|
|
19315
21038
|
if (exitCode !== null && exitCode !== 0) {
|
|
19316
21039
|
throw new ChannelAValidationError(
|
|
@@ -19335,7 +21058,7 @@ var SandboxChannelAService = class {
|
|
|
19335
21058
|
rejectFinalSymlink: true
|
|
19336
21059
|
});
|
|
19337
21060
|
const flag = req.recursive ? "-p " : "";
|
|
19338
|
-
const { exitCode, stderr } = await this.run({ cmd: `mkdir ${flag}${
|
|
21061
|
+
const { exitCode, stderr } = await this.run({ cmd: `mkdir ${flag}${shellQuote6(abs)}` });
|
|
19339
21062
|
if (exitCode !== null && exitCode !== 0) {
|
|
19340
21063
|
throw new ChannelAValidationError(`failed to mkdir ${path}: ${stderr || `exit ${exitCode}`}`);
|
|
19341
21064
|
}
|
|
@@ -19371,10 +21094,10 @@ var SandboxChannelAService = class {
|
|
|
19371
21094
|
const repo = assertSafeRelPathOrRoot(req.path);
|
|
19372
21095
|
const ctx = req.contextLines;
|
|
19373
21096
|
let range = "";
|
|
19374
|
-
if (req.fromRef && req.toRef) range = `${
|
|
19375
|
-
else if (req.fromRef) range = `${
|
|
21097
|
+
if (req.fromRef && req.toRef) range = `${shellQuote6(req.fromRef)} ${shellQuote6(req.toRef)}`;
|
|
21098
|
+
else if (req.fromRef) range = `${shellQuote6(req.fromRef)}`;
|
|
19376
21099
|
else if (req.staged) range = "--cached";
|
|
19377
|
-
const pathspec = req.pathspec.length ? ` -- ${req.pathspec.map(
|
|
21100
|
+
const pathspec = req.pathspec.length ? ` -- ${req.pathspec.map(shellQuote6).join(" ")}` : "";
|
|
19378
21101
|
const numstat = await this.runInConfinedDirectory(repo, {
|
|
19379
21102
|
cmd: `git -c core.quotePath=false diff --no-color -z --numstat ${range}${pathspec}`.trim()
|
|
19380
21103
|
});
|
|
@@ -19398,7 +21121,7 @@ var SandboxChannelAService = class {
|
|
|
19398
21121
|
continue;
|
|
19399
21122
|
}
|
|
19400
21123
|
const patch = await this.runInConfinedDirectory(repo, {
|
|
19401
|
-
cmd: `git -c core.quotePath=false diff --no-color -U${ctx} ${range} -- ${
|
|
21124
|
+
cmd: `git -c core.quotePath=false diff --no-color -U${ctx} ${range} -- ${shellQuote6(target)}`.trim()
|
|
19402
21125
|
});
|
|
19403
21126
|
const oversized = Buffer.byteLength(patch.stdout, "utf8") > req.maxBytesPerFile;
|
|
19404
21127
|
const parsed = oversized ? { hunks: [], status: "modified" } : parseUnifiedPatch(patch.stdout);
|
|
@@ -19422,7 +21145,7 @@ var SandboxChannelAService = class {
|
|
|
19422
21145
|
const fileArg = target.startsWith("./") ? target : `./${target}`;
|
|
19423
21146
|
const read = await this.runInConfinedDirectory(repo, {
|
|
19424
21147
|
cmd: [
|
|
19425
|
-
`file=${
|
|
21148
|
+
`file=${shellQuote6(fileArg)}`,
|
|
19426
21149
|
'if [ -L "$file" ]; then printf "L\\n"; readlink -z -- "$file" | base64 | tr -d "\\n"; exit; fi',
|
|
19427
21150
|
'size=$(wc -c < "$file") || exit 66',
|
|
19428
21151
|
'line_count=$(wc -l < "$file") || exit 66',
|
|
@@ -19486,9 +21209,9 @@ var SandboxChannelAService = class {
|
|
|
19486
21209
|
async gitLog(req) {
|
|
19487
21210
|
const repo = assertSafeRelPathOrRoot(req.path);
|
|
19488
21211
|
const fmt = `%H${US}%h${US}%P${US}%an${US}%ae${US}%at${US}%cn${US}%ce${US}%ct${US}%s${US}%b${RS}`;
|
|
19489
|
-
const pathspec = req.pathspec.length ? ` -- ${req.pathspec.map(
|
|
21212
|
+
const pathspec = req.pathspec.length ? ` -- ${req.pathspec.map(shellQuote6).join(" ")}` : "";
|
|
19490
21213
|
const { stdout, exitCode } = await this.runInConfinedDirectory(repo, {
|
|
19491
|
-
cmd: `git log --format=${
|
|
21214
|
+
cmd: `git log --format=${shellQuote6(fmt)} -n${req.maxCount + 1} --skip=${req.skip} ${shellQuote6(req.ref)}${pathspec}`
|
|
19492
21215
|
});
|
|
19493
21216
|
if (exitCode !== null && exitCode !== 0) {
|
|
19494
21217
|
return { commits: [], hasMore: false };
|
|
@@ -19515,7 +21238,7 @@ var SandboxChannelAService = class {
|
|
|
19515
21238
|
const repo = assertSafeRelPathOrRoot(req.path);
|
|
19516
21239
|
if (req.filePath) {
|
|
19517
21240
|
const { stdout, exitCode } = await this.runInConfinedDirectory(repo, {
|
|
19518
|
-
cmd: `git cat-file blob ${
|
|
21241
|
+
cmd: `git cat-file blob ${shellQuote6(`${req.ref}:${req.filePath}`)} 2>/dev/null | base64`
|
|
19519
21242
|
});
|
|
19520
21243
|
if (exitCode !== null && exitCode !== 0 && stdout.trim() === "") {
|
|
19521
21244
|
throw new ChannelANotFoundError(`blob not found: ${req.ref}:${req.filePath}`);
|
|
@@ -19646,17 +21369,49 @@ var SandboxChannelAService = class {
|
|
|
19646
21369
|
return (await this.detectReposDetailed()).repos;
|
|
19647
21370
|
}
|
|
19648
21371
|
// ════════════════════════ Terminal exec + PTY (A2) ════════════════════════
|
|
19649
|
-
/** Run a bounded command
|
|
19650
|
-
*
|
|
19651
|
-
*
|
|
19652
|
-
* 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. */
|
|
19653
21375
|
async terminalExec(req) {
|
|
19654
|
-
const
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
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
|
+
}
|
|
19660
21415
|
if (req.emitStream && (r.stdout || r.stderr)) {
|
|
19661
21416
|
const events = [];
|
|
19662
21417
|
const commandId = crypto.randomUUID();
|
|
@@ -19676,7 +21431,7 @@ var SandboxChannelAService = class {
|
|
|
19676
21431
|
stdout: r.stdout,
|
|
19677
21432
|
stderr: r.stderr,
|
|
19678
21433
|
exitCode: r.exitCode,
|
|
19679
|
-
running,
|
|
21434
|
+
running: false,
|
|
19680
21435
|
wallTimeSeconds: r.wallTimeSeconds
|
|
19681
21436
|
};
|
|
19682
21437
|
}
|
|
@@ -19706,10 +21461,11 @@ var SandboxChannelAService = class {
|
|
|
19706
21461
|
* it as terminal.pty.output.delta). Throws ChannelAUnsupportedError when the
|
|
19707
21462
|
* backend has no writeStdin. */
|
|
19708
21463
|
async ptyWrite(_req, execSessionId, data) {
|
|
19709
|
-
|
|
21464
|
+
const write = this.session.writeStdinForProcessMutation?.bind(this.session) ?? this.session.writeStdin?.bind(this.session);
|
|
21465
|
+
if (!write) {
|
|
19710
21466
|
throw new ChannelAUnsupportedError("interactive terminal unsupported on this backend");
|
|
19711
21467
|
}
|
|
19712
|
-
const out = await
|
|
21468
|
+
const out = await write({
|
|
19713
21469
|
sessionId: execSessionId,
|
|
19714
21470
|
chars: data,
|
|
19715
21471
|
yieldTimeMs: 250
|
|
@@ -19722,23 +21478,22 @@ var SandboxChannelAService = class {
|
|
|
19722
21478
|
/** Resize an open PTY (SIGWINCH via stty against the exec-session). The SDK has
|
|
19723
21479
|
* no resize method; stty in the same tty session updates the geometry. */
|
|
19724
21480
|
async ptyResize(req, execSessionId) {
|
|
19725
|
-
|
|
19726
|
-
|
|
21481
|
+
const write = this.session.writeStdinForProcessControl?.bind(this.session) ?? this.session.writeStdin?.bind(this.session);
|
|
21482
|
+
if (!write) return;
|
|
21483
|
+
await write({
|
|
19727
21484
|
sessionId: execSessionId,
|
|
19728
21485
|
chars: `stty cols ${req.cols} rows ${req.rows}
|
|
19729
21486
|
`,
|
|
19730
21487
|
yieldTimeMs: 50
|
|
19731
21488
|
});
|
|
19732
21489
|
}
|
|
19733
|
-
/**
|
|
19734
|
-
*
|
|
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. */
|
|
19735
21493
|
async ptyClose(_req, execSessionId) {
|
|
19736
|
-
|
|
19737
|
-
|
|
19738
|
-
|
|
19739
|
-
} catch {
|
|
19740
|
-
}
|
|
19741
|
-
}
|
|
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 });
|
|
19742
21497
|
}
|
|
19743
21498
|
// ──────────────────────────── helpers ──────────────────────────────────────
|
|
19744
21499
|
/** The current FS revision (for the caller to persist/seed). */
|
|
@@ -19756,11 +21511,11 @@ var SandboxChannelAService = class {
|
|
|
19756
21511
|
const safe = assertSafeRelPathOrRoot(rel);
|
|
19757
21512
|
const root = this.workspaceRoot || ".";
|
|
19758
21513
|
const abs = this.joinRoot(safe);
|
|
19759
|
-
const rejectLink = safe ? `test ! -L ${
|
|
21514
|
+
const rejectLink = safe ? `test ! -L ${shellQuote6(abs)} || { printf '__OPENGENI_FS_SYMLINK__'; exit 68; }` : ":";
|
|
19760
21515
|
const script = [
|
|
19761
|
-
`root=$(realpath -e -- ${
|
|
21516
|
+
`root=$(realpath -e -- ${shellQuote6(root)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19762
21517
|
rejectLink,
|
|
19763
|
-
`target=$(realpath -e -- ${
|
|
21518
|
+
`target=$(realpath -e -- ${shellQuote6(abs)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19764
21519
|
`case "$target" in "$root"|"$root"/*) ;; *) printf '__OPENGENI_FS_ESCAPE__'; exit 67 ;; esac`,
|
|
19765
21520
|
`test -d "$target" || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
|
|
19766
21521
|
`cd -P -- "$target"`,
|
|
@@ -19870,28 +21625,6 @@ function isDefinitePathNotFoundError(error) {
|
|
|
19870
21625
|
const candidate = error;
|
|
19871
21626
|
return candidate.osNotFound === true || candidate.code === "ENOENT" || candidate.errno === "ENOENT" || candidate.errno === -2;
|
|
19872
21627
|
}
|
|
19873
|
-
function isExecSessionLostBanner(out, execSessionId) {
|
|
19874
|
-
if (!out || !Number.isSafeInteger(execSessionId) || execSessionId < 0) return false;
|
|
19875
|
-
const match = out.match(/^(?:write_stdin failed: )?session not found: (\d+)$/);
|
|
19876
|
-
if (!match || match[0] !== out) return false;
|
|
19877
|
-
return match[1] === String(execSessionId);
|
|
19878
|
-
}
|
|
19879
|
-
function parseExecBannerSessionId(raw) {
|
|
19880
|
-
const outputIdx = raw.indexOf("\nOutput:\n");
|
|
19881
|
-
const banner = outputIdx >= 0 ? raw.slice(0, outputIdx) : raw.startsWith("Output:\n") ? "" : raw;
|
|
19882
|
-
const match = banner.match(/Process running with session ID (\d+)/);
|
|
19883
|
-
if (!match) return null;
|
|
19884
|
-
const n = Number.parseInt(match[1], 10);
|
|
19885
|
-
return Number.isFinite(n) ? n : null;
|
|
19886
|
-
}
|
|
19887
|
-
function parseExecBannerExitCode(raw) {
|
|
19888
|
-
const outputIdx = raw.indexOf("\nOutput:\n");
|
|
19889
|
-
const banner = outputIdx >= 0 ? raw.slice(0, outputIdx) : raw.startsWith("Output:\n") ? "" : raw;
|
|
19890
|
-
const match = banner.match(/Process exited with code (-?\d+)/);
|
|
19891
|
-
if (!match) return null;
|
|
19892
|
-
const exitCode = Number.parseInt(match[1], 10);
|
|
19893
|
-
return Number.isSafeInteger(exitCode) ? exitCode : null;
|
|
19894
|
-
}
|
|
19895
21628
|
function sniffBinary(bytes) {
|
|
19896
21629
|
const n = Math.min(bytes.byteLength, 8192);
|
|
19897
21630
|
for (let i = 0; i < n; i++) if (bytes[i] === 0) return true;
|
|
@@ -19925,11 +21658,11 @@ function assertSafeRelPath(p) {
|
|
|
19925
21658
|
if (norm === "") throw new ChannelAValidationError("path is required");
|
|
19926
21659
|
return norm;
|
|
19927
21660
|
}
|
|
19928
|
-
function
|
|
21661
|
+
function shellQuote6(s) {
|
|
19929
21662
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19930
21663
|
}
|
|
19931
21664
|
function internalBashCommand(script) {
|
|
19932
|
-
return `env -u BASH_ENV bash --noprofile --norc -c ${
|
|
21665
|
+
return `env -u BASH_ENV bash --noprofile --norc -c ${shellQuote6(script)}`;
|
|
19933
21666
|
}
|
|
19934
21667
|
function assertSafePruneDirectoryName(name) {
|
|
19935
21668
|
if (name.length > 255 || !/^[A-Za-z0-9._@+-]+$/.test(name) || name === "." || name === "..") {
|
|
@@ -20463,12 +22196,12 @@ function defaultEcho(req, hostname) {
|
|
|
20463
22196
|
durationMs: "1"
|
|
20464
22197
|
};
|
|
20465
22198
|
}
|
|
20466
|
-
function ok(
|
|
20467
|
-
return { requestId, error: void 0, result };
|
|
22199
|
+
function ok(requestId2, result) {
|
|
22200
|
+
return { requestId: requestId2, error: void 0, result };
|
|
20468
22201
|
}
|
|
20469
|
-
function errorResponse(
|
|
22202
|
+
function errorResponse(requestId2, code, message, retryable) {
|
|
20470
22203
|
const error = { code, message, retryable, detail: {} };
|
|
20471
|
-
return { requestId, error, result: void 0 };
|
|
22204
|
+
return { requestId: requestId2, error, result: void 0 };
|
|
20472
22205
|
}
|
|
20473
22206
|
function normalize(path) {
|
|
20474
22207
|
const trimmed = path.replace(/\/+$/, "");
|
|
@@ -20488,6 +22221,35 @@ var RoutingUnsupportedError = class extends Error {
|
|
|
20488
22221
|
super(`the active sandbox (${kind}) does not support "${op}"`);
|
|
20489
22222
|
}
|
|
20490
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
|
+
};
|
|
20491
22253
|
function isFenceError(error) {
|
|
20492
22254
|
if (!error || typeof error !== "object") {
|
|
20493
22255
|
return false;
|
|
@@ -20500,6 +22262,59 @@ function isFenceError(error) {
|
|
|
20500
22262
|
const haystack = `${name} ${message}`.toLowerCase();
|
|
20501
22263
|
return haystack.includes("fenced") || haystack.includes("epoch") && haystack.includes("super");
|
|
20502
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
|
+
}
|
|
20503
22318
|
var RoutingSandboxSession = class {
|
|
20504
22319
|
deps;
|
|
20505
22320
|
maxFenceRetries;
|
|
@@ -20518,6 +22333,10 @@ var RoutingSandboxSession = class {
|
|
|
20518
22333
|
// The last-resolved backend, exposed via the `state` getter (a method-free read
|
|
20519
22334
|
// of the active backend's `state`). Updated on every resolve.
|
|
20520
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();
|
|
20521
22340
|
// The native-desktop control-plane ops (self-hosted / macOS). Declared as OPTIONAL
|
|
20522
22341
|
// INSTANCE fields — NOT prototype methods — because their PRESENCE is the selection
|
|
20523
22342
|
// signal `isNativeDesktopSession` (sandbox-computer.ts) uses to pick the native vs
|
|
@@ -20533,13 +22352,13 @@ var RoutingSandboxSession = class {
|
|
|
20533
22352
|
this.maxFenceRetries = deps.maxFenceRetries ?? 3;
|
|
20534
22353
|
const def = deps.defaultResolved?.session;
|
|
20535
22354
|
if (typeof def?.desktopInput === "function" && typeof def?.screenshot === "function") {
|
|
20536
|
-
this.desktopInput = (event) => this.dispatch("desktopInput", async (s) => {
|
|
22355
|
+
this.desktopInput = (event) => this.dispatch("desktopInput", true, async (s) => {
|
|
20537
22356
|
if (!s.desktopInput) {
|
|
20538
22357
|
throw new RoutingUnsupportedError("desktopInput", this.cached?.kind ?? "unknown");
|
|
20539
22358
|
}
|
|
20540
22359
|
return s.desktopInput(event);
|
|
20541
22360
|
});
|
|
20542
|
-
this.screenshot = () => this.dispatch("screenshot", async (s) => {
|
|
22361
|
+
this.screenshot = () => this.dispatch("screenshot", false, async (s) => {
|
|
20543
22362
|
if (!s.screenshot) {
|
|
20544
22363
|
throw new RoutingUnsupportedError("screenshot", this.cached?.kind ?? "unknown");
|
|
20545
22364
|
}
|
|
@@ -20584,18 +22403,256 @@ var RoutingSandboxSession = class {
|
|
|
20584
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."
|
|
20585
22404
|
);
|
|
20586
22405
|
}
|
|
22406
|
+
const routed = {
|
|
22407
|
+
...resolved,
|
|
22408
|
+
activeEpoch: pointer.activeEpoch
|
|
22409
|
+
};
|
|
20587
22410
|
this.cachedEpoch = pointer.activeEpoch;
|
|
20588
22411
|
this.cachedSandboxId = pointer.activeSandboxId;
|
|
20589
|
-
this.cached =
|
|
20590
|
-
this.lastResolved =
|
|
22412
|
+
this.cached = routed;
|
|
22413
|
+
this.lastResolved = routed;
|
|
20591
22414
|
this.deps.onTransition?.({
|
|
20592
22415
|
type: this.cachedEpoch !== void 0 && fromEpoch !== pointer.activeEpoch ? "epoch-changed" : "resolved",
|
|
20593
22416
|
fromEpoch,
|
|
20594
22417
|
toEpoch: pointer.activeEpoch,
|
|
20595
|
-
sandboxId:
|
|
20596
|
-
kind:
|
|
22418
|
+
sandboxId: routed.sandboxId,
|
|
22419
|
+
kind: routed.kind
|
|
20597
22420
|
});
|
|
20598
|
-
return
|
|
22421
|
+
return routed;
|
|
22422
|
+
}
|
|
22423
|
+
invalidate(backend) {
|
|
22424
|
+
this.cachedEpoch = void 0;
|
|
22425
|
+
this.cachedSandboxId = void 0;
|
|
22426
|
+
this.cached = void 0;
|
|
22427
|
+
this.deps.onTransition?.({
|
|
22428
|
+
type: "fenced-retry",
|
|
22429
|
+
fromEpoch: backend.activeEpoch ?? 0,
|
|
22430
|
+
toEpoch: 0,
|
|
22431
|
+
sandboxId: backend.sandboxId,
|
|
22432
|
+
kind: backend.kind
|
|
22433
|
+
});
|
|
22434
|
+
}
|
|
22435
|
+
registerRetainedProcess(process2, backend) {
|
|
22436
|
+
if (this.retainedProcesses.has(process2.providerSessionId)) {
|
|
22437
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22438
|
+
"retainProcess",
|
|
22439
|
+
`Provider session ${process2.providerSessionId} was yielded while that locator was already retained; neither process was rebound`
|
|
22440
|
+
);
|
|
22441
|
+
}
|
|
22442
|
+
const record = {
|
|
22443
|
+
process: process2,
|
|
22444
|
+
backend,
|
|
22445
|
+
durable: !this.deps.afterMutation,
|
|
22446
|
+
pendingParentPromotion: null,
|
|
22447
|
+
pendingMutationSettlement: null,
|
|
22448
|
+
pendingTerminal: null,
|
|
22449
|
+
settlement: null
|
|
22450
|
+
};
|
|
22451
|
+
this.retainedProcesses.set(process2.providerSessionId, record);
|
|
22452
|
+
return record;
|
|
22453
|
+
}
|
|
22454
|
+
retainedProcess(providerSessionId) {
|
|
22455
|
+
const retained = this.retainedProcesses.get(providerSessionId);
|
|
22456
|
+
if (!retained) throw new RoutingRetainedProcessNotFoundError(providerSessionId);
|
|
22457
|
+
return retained;
|
|
22458
|
+
}
|
|
22459
|
+
/** Reconstruct one process that was already promoted durably by an earlier
|
|
22460
|
+
* request. Adoption never reads the active pointer and never repeats parent
|
|
22461
|
+
* promotion. It can bind only to the exact construction-time default backend;
|
|
22462
|
+
* callers must reject non-default routes until they can independently rebuild
|
|
22463
|
+
* that provider session from its copied durable identity. */
|
|
22464
|
+
adoptRetainedProcess(input) {
|
|
22465
|
+
const providerSessionId = positiveProviderSessionId(input.process.providerSessionId);
|
|
22466
|
+
const seed = this.deps.defaultResolved;
|
|
22467
|
+
if (providerSessionId === null || !canonicalUuid(input.process.id) || !seed || seed.sandboxId !== input.backend.sandboxId || seed.leaseEpoch !== input.backend.leaseEpoch || seed.providerInstanceId !== input.backend.providerInstanceId || !Number.isSafeInteger(input.backend.activeEpoch) || input.backend.activeEpoch < 0) {
|
|
22468
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22469
|
+
"adoptRetainedProcess",
|
|
22470
|
+
`Retained provider session ${input.process.providerSessionId} did not match the exact default backend identity`
|
|
22471
|
+
);
|
|
22472
|
+
}
|
|
22473
|
+
const existing = this.retainedProcesses.get(providerSessionId);
|
|
22474
|
+
if (existing) {
|
|
22475
|
+
if (existing.process.id === input.process.id && existing.backend.session === seed.session && existing.backend.sandboxId === input.backend.sandboxId && existing.backend.leaseEpoch === input.backend.leaseEpoch && existing.backend.providerInstanceId === input.backend.providerInstanceId && existing.backend.activeEpoch === input.backend.activeEpoch) {
|
|
22476
|
+
return;
|
|
22477
|
+
}
|
|
22478
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22479
|
+
"adoptRetainedProcess",
|
|
22480
|
+
`Provider session ${providerSessionId} is already bound to a different retained process identity`
|
|
22481
|
+
);
|
|
22482
|
+
}
|
|
22483
|
+
this.retainedProcesses.set(providerSessionId, {
|
|
22484
|
+
process: { ...input.process },
|
|
22485
|
+
backend: {
|
|
22486
|
+
...seed,
|
|
22487
|
+
activeEpoch: input.backend.activeEpoch
|
|
22488
|
+
},
|
|
22489
|
+
durable: true,
|
|
22490
|
+
pendingParentPromotion: null,
|
|
22491
|
+
pendingMutationSettlement: null,
|
|
22492
|
+
pendingTerminal: null,
|
|
22493
|
+
settlement: null
|
|
22494
|
+
});
|
|
22495
|
+
}
|
|
22496
|
+
async ensureParentPromotion(record) {
|
|
22497
|
+
if (record.durable) return;
|
|
22498
|
+
const pending = record.pendingParentPromotion;
|
|
22499
|
+
if (!pending || !this.deps.afterMutation) {
|
|
22500
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22501
|
+
"retainProcess",
|
|
22502
|
+
`Retained provider session ${record.process.providerSessionId} has no confirmed durable parent admission`
|
|
22503
|
+
);
|
|
22504
|
+
}
|
|
22505
|
+
try {
|
|
22506
|
+
await this.deps.afterMutation(pending);
|
|
22507
|
+
record.pendingParentPromotion = null;
|
|
22508
|
+
record.durable = true;
|
|
22509
|
+
} catch (error) {
|
|
22510
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22511
|
+
pending.op,
|
|
22512
|
+
`Yielded sandbox process ${record.process.providerSessionId} could not confirm its durable promotion; tracking remains pinned to the original backend`,
|
|
22513
|
+
{ cause: error }
|
|
22514
|
+
);
|
|
22515
|
+
}
|
|
22516
|
+
}
|
|
22517
|
+
async settleRetainedProcess(record, proof, result) {
|
|
22518
|
+
record.pendingTerminal ??= { proof, result };
|
|
22519
|
+
const pending = record.pendingTerminal;
|
|
22520
|
+
if (pending.proof.outcome !== proof.outcome || pending.proof.exitCode !== proof.exitCode || pending.proof.reason !== proof.reason) {
|
|
22521
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22522
|
+
"settleProcess",
|
|
22523
|
+
`Retained provider session ${record.process.providerSessionId} produced conflicting terminal proof`
|
|
22524
|
+
);
|
|
22525
|
+
}
|
|
22526
|
+
record.settlement ??= (async () => {
|
|
22527
|
+
await this.ensureParentPromotion(record);
|
|
22528
|
+
await this.deps.settleProcess?.({
|
|
22529
|
+
backend: record.backend,
|
|
22530
|
+
process: record.process,
|
|
22531
|
+
proof: pending.proof
|
|
22532
|
+
});
|
|
22533
|
+
this.retainedProcesses.delete(record.process.providerSessionId);
|
|
22534
|
+
record.pendingTerminal = null;
|
|
22535
|
+
})();
|
|
22536
|
+
try {
|
|
22537
|
+
await record.settlement;
|
|
22538
|
+
} catch (error) {
|
|
22539
|
+
record.settlement = null;
|
|
22540
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22541
|
+
"settleProcess",
|
|
22542
|
+
`Retained provider session ${record.process.providerSessionId} reached a terminal state but durable settlement failed; tracking remains pinned`,
|
|
22543
|
+
{ cause: error }
|
|
22544
|
+
);
|
|
22545
|
+
}
|
|
22546
|
+
}
|
|
22547
|
+
async flushPendingProcessMutation(record) {
|
|
22548
|
+
const pending = record.pendingMutationSettlement;
|
|
22549
|
+
if (!pending) return null;
|
|
22550
|
+
try {
|
|
22551
|
+
await this.deps.afterProcessMutation?.(pending);
|
|
22552
|
+
record.pendingMutationSettlement = null;
|
|
22553
|
+
} catch (error) {
|
|
22554
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22555
|
+
pending.op,
|
|
22556
|
+
`Retained-process mutation ${pending.op} still lacks durable physical settlement; no later process mutation was admitted`,
|
|
22557
|
+
{ cause: error }
|
|
22558
|
+
);
|
|
22559
|
+
}
|
|
22560
|
+
if (pending.outcome === "resolved" && typeof pending.result === "string") {
|
|
22561
|
+
const proof = retainedProcessTerminalProof(pending.result, record.process.providerSessionId);
|
|
22562
|
+
if (proof) {
|
|
22563
|
+
await this.settleRetainedProcess(record, proof, pending.result);
|
|
22564
|
+
return pending.result;
|
|
22565
|
+
}
|
|
22566
|
+
}
|
|
22567
|
+
return null;
|
|
22568
|
+
}
|
|
22569
|
+
async dispatchProcessMutation(args) {
|
|
22570
|
+
const providerSessionId = providerSessionIdFromArgs(args);
|
|
22571
|
+
if (providerSessionId === null) throw new RoutingRetainedProcessNotFoundError(-1);
|
|
22572
|
+
const record = this.retainedProcess(providerSessionId);
|
|
22573
|
+
const priorTerminal = await this.flushPendingProcessMutation(record);
|
|
22574
|
+
if (priorTerminal !== null) return priorTerminal;
|
|
22575
|
+
if (record.pendingTerminal) {
|
|
22576
|
+
const terminal = record.pendingTerminal;
|
|
22577
|
+
await this.settleRetainedProcess(record, terminal.proof, terminal.result);
|
|
22578
|
+
return terminal.result;
|
|
22579
|
+
}
|
|
22580
|
+
await this.ensureParentPromotion(record);
|
|
22581
|
+
const op = "writeStdin";
|
|
22582
|
+
const admission = await this.deps.beforeProcessMutation?.({
|
|
22583
|
+
op,
|
|
22584
|
+
backend: record.backend,
|
|
22585
|
+
process: record.process
|
|
22586
|
+
});
|
|
22587
|
+
const write = record.backend.session.writeStdin;
|
|
22588
|
+
if (!write) throw new RoutingUnsupportedError(op, record.backend.kind);
|
|
22589
|
+
let result;
|
|
22590
|
+
try {
|
|
22591
|
+
result = await write.call(record.backend.session, args);
|
|
22592
|
+
} catch (error) {
|
|
22593
|
+
if (this.deps.afterProcessMutation) {
|
|
22594
|
+
const pending = {
|
|
22595
|
+
op,
|
|
22596
|
+
backend: record.backend,
|
|
22597
|
+
process: record.process,
|
|
22598
|
+
admission,
|
|
22599
|
+
outcome: "rejected"
|
|
22600
|
+
};
|
|
22601
|
+
try {
|
|
22602
|
+
await this.deps.afterProcessMutation(pending);
|
|
22603
|
+
} catch (settlementError) {
|
|
22604
|
+
record.pendingMutationSettlement = pending;
|
|
22605
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22606
|
+
op,
|
|
22607
|
+
`Retained-process stdin rejected at the provider but lost durable settlement; it was not replayed`,
|
|
22608
|
+
{ cause: settlementError }
|
|
22609
|
+
);
|
|
22610
|
+
}
|
|
22611
|
+
}
|
|
22612
|
+
throw error;
|
|
22613
|
+
}
|
|
22614
|
+
if (this.deps.afterProcessMutation) {
|
|
22615
|
+
const pending = {
|
|
22616
|
+
op,
|
|
22617
|
+
backend: record.backend,
|
|
22618
|
+
process: record.process,
|
|
22619
|
+
admission,
|
|
22620
|
+
outcome: "resolved",
|
|
22621
|
+
result
|
|
22622
|
+
};
|
|
22623
|
+
try {
|
|
22624
|
+
await this.deps.afterProcessMutation(pending);
|
|
22625
|
+
} catch (error) {
|
|
22626
|
+
record.pendingMutationSettlement = pending;
|
|
22627
|
+
throw new RoutingMutationOutcomeUnknownError(
|
|
22628
|
+
op,
|
|
22629
|
+
`Retained-process stdin returned from the provider but lost durable settlement; tracking remains pinned and it was not replayed`,
|
|
22630
|
+
{ cause: error }
|
|
22631
|
+
);
|
|
22632
|
+
}
|
|
22633
|
+
}
|
|
22634
|
+
const proof = retainedProcessTerminalProof(result, providerSessionId);
|
|
22635
|
+
if (proof) await this.settleRetainedProcess(record, proof, result);
|
|
22636
|
+
return result;
|
|
22637
|
+
}
|
|
22638
|
+
async dispatchProcessControl(args) {
|
|
22639
|
+
const providerSessionId = providerSessionIdFromArgs(args);
|
|
22640
|
+
if (providerSessionId === null) throw new RoutingRetainedProcessNotFoundError(-1);
|
|
22641
|
+
const record = this.retainedProcess(providerSessionId);
|
|
22642
|
+
const priorTerminal = await this.flushPendingProcessMutation(record);
|
|
22643
|
+
if (priorTerminal !== null) return priorTerminal;
|
|
22644
|
+
if (record.pendingTerminal) {
|
|
22645
|
+
const terminal = record.pendingTerminal;
|
|
22646
|
+
await this.settleRetainedProcess(record, terminal.proof, terminal.result);
|
|
22647
|
+
return terminal.result;
|
|
22648
|
+
}
|
|
22649
|
+
await this.ensureParentPromotion(record);
|
|
22650
|
+
const write = record.backend.session.writeStdin;
|
|
22651
|
+
if (!write) throw new RoutingUnsupportedError("writeStdin", record.backend.kind);
|
|
22652
|
+
const result = await write.call(record.backend.session, args);
|
|
22653
|
+
const proof = retainedProcessTerminalProof(result, providerSessionId);
|
|
22654
|
+
if (proof) await this.settleRetainedProcess(record, proof, result);
|
|
22655
|
+
return result;
|
|
20599
22656
|
}
|
|
20600
22657
|
/**
|
|
20601
22658
|
* Dispatch an op to the currently-active backend, retrying on a stale-epoch
|
|
@@ -20608,30 +22665,113 @@ var RoutingSandboxSession = class {
|
|
|
20608
22665
|
* A non-fence error propagates immediately (it is a real op failure, not a swap
|
|
20609
22666
|
* race).
|
|
20610
22667
|
*/
|
|
20611
|
-
async dispatch(op, fn) {
|
|
22668
|
+
async dispatch(op, mutatesWorkspace, fn) {
|
|
20612
22669
|
let attempt = 0;
|
|
20613
22670
|
let lastError;
|
|
20614
22671
|
while (attempt <= this.maxFenceRetries) {
|
|
20615
22672
|
const backend = await this.resolve();
|
|
22673
|
+
const admission = mutatesWorkspace ? await this.deps.beforeMutation?.({ op, backend }) : void 0;
|
|
22674
|
+
let result;
|
|
20616
22675
|
try {
|
|
20617
|
-
|
|
22676
|
+
result = await fn(backend.session);
|
|
20618
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
|
+
}
|
|
20619
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
|
+
}
|
|
20620
22708
|
throw error;
|
|
20621
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
|
+
}
|
|
20622
22718
|
lastError = error;
|
|
20623
|
-
this.cachedEpoch = void 0;
|
|
20624
|
-
this.cachedSandboxId = void 0;
|
|
20625
|
-
this.cached = void 0;
|
|
20626
|
-
this.deps.onTransition?.({
|
|
20627
|
-
type: "fenced-retry",
|
|
20628
|
-
fromEpoch: backend.sandboxId === null ? 0 : 0,
|
|
20629
|
-
toEpoch: 0,
|
|
20630
|
-
sandboxId: backend.sandboxId,
|
|
20631
|
-
kind: backend.kind
|
|
20632
|
-
});
|
|
20633
22719
|
attempt += 1;
|
|
22720
|
+
continue;
|
|
20634
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;
|
|
22773
|
+
}
|
|
22774
|
+
return result;
|
|
20635
22775
|
}
|
|
20636
22776
|
throw lastError ?? new Error(`routing op "${op}" exhausted fence retries`);
|
|
20637
22777
|
}
|
|
@@ -20664,7 +22804,7 @@ var RoutingSandboxSession = class {
|
|
|
20664
22804
|
// dispatches to the active backend at call-time. A missing backend method
|
|
20665
22805
|
// degrades via the natural fallback or RoutingUnsupportedError.
|
|
20666
22806
|
async exec(args) {
|
|
20667
|
-
return this.dispatch("exec", async (s) => {
|
|
22807
|
+
return this.dispatch("exec", true, async (s) => {
|
|
20668
22808
|
if (s.exec) {
|
|
20669
22809
|
return s.exec(args);
|
|
20670
22810
|
}
|
|
@@ -20676,7 +22816,7 @@ var RoutingSandboxSession = class {
|
|
|
20676
22816
|
}
|
|
20677
22817
|
async execCommand(args) {
|
|
20678
22818
|
try {
|
|
20679
|
-
return await this.dispatch("execCommand", async (s) => {
|
|
22819
|
+
return await this.dispatch("execCommand", true, async (s) => {
|
|
20680
22820
|
if (s.execCommand) {
|
|
20681
22821
|
return s.execCommand(args);
|
|
20682
22822
|
}
|
|
@@ -20691,21 +22831,72 @@ var RoutingSandboxSession = class {
|
|
|
20691
22831
|
}
|
|
20692
22832
|
}
|
|
20693
22833
|
async writeStdin(args) {
|
|
20694
|
-
|
|
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) => {
|
|
20695
22839
|
if (!s.writeStdin) {
|
|
20696
22840
|
throw new RoutingUnsupportedError("writeStdin", this.cached?.kind ?? "unknown");
|
|
20697
22841
|
}
|
|
20698
22842
|
return s.writeStdin(args);
|
|
20699
22843
|
});
|
|
20700
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
|
+
}
|
|
20701
22892
|
async cancelExecCommand(opId) {
|
|
20702
|
-
return await this.dispatch("cancelExecCommand", async (session) => {
|
|
22893
|
+
return await this.dispatch("cancelExecCommand", false, async (session) => {
|
|
20703
22894
|
if (!session.cancelExecCommand) return false;
|
|
20704
22895
|
return await session.cancelExecCommand(opId);
|
|
20705
22896
|
});
|
|
20706
22897
|
}
|
|
20707
22898
|
async readFile(args) {
|
|
20708
|
-
return this.dispatch("readFile", async (s) => {
|
|
22899
|
+
return this.dispatch("readFile", false, async (s) => {
|
|
20709
22900
|
if (!s.readFile) {
|
|
20710
22901
|
throw new RoutingUnsupportedError("readFile", this.cached?.kind ?? "unknown");
|
|
20711
22902
|
}
|
|
@@ -20713,7 +22904,7 @@ var RoutingSandboxSession = class {
|
|
|
20713
22904
|
});
|
|
20714
22905
|
}
|
|
20715
22906
|
async writeFile(args) {
|
|
20716
|
-
return this.dispatch("writeFile", async (s) => {
|
|
22907
|
+
return this.dispatch("writeFile", true, async (s) => {
|
|
20717
22908
|
if (!s.writeFile) {
|
|
20718
22909
|
throw new RoutingUnsupportedError("writeFile", this.cached?.kind ?? "unknown");
|
|
20719
22910
|
}
|
|
@@ -20721,7 +22912,7 @@ var RoutingSandboxSession = class {
|
|
|
20721
22912
|
});
|
|
20722
22913
|
}
|
|
20723
22914
|
async listDir(args) {
|
|
20724
|
-
return this.dispatch("listDir", async (s) => {
|
|
22915
|
+
return this.dispatch("listDir", false, async (s) => {
|
|
20725
22916
|
if (!s.listDir) {
|
|
20726
22917
|
throw new RoutingUnsupportedError("listDir", this.cached?.kind ?? "unknown");
|
|
20727
22918
|
}
|
|
@@ -20729,7 +22920,7 @@ var RoutingSandboxSession = class {
|
|
|
20729
22920
|
});
|
|
20730
22921
|
}
|
|
20731
22922
|
async pathExists(path, runAs) {
|
|
20732
|
-
return this.dispatch("pathExists", async (s) => {
|
|
22923
|
+
return this.dispatch("pathExists", false, async (s) => {
|
|
20733
22924
|
if (!s.pathExists) {
|
|
20734
22925
|
throw new RoutingUnsupportedError("pathExists", this.cached?.kind ?? "unknown");
|
|
20735
22926
|
}
|
|
@@ -20737,7 +22928,7 @@ var RoutingSandboxSession = class {
|
|
|
20737
22928
|
});
|
|
20738
22929
|
}
|
|
20739
22930
|
async viewImage(args) {
|
|
20740
|
-
return this.dispatch("viewImage", async (s) => {
|
|
22931
|
+
return this.dispatch("viewImage", false, async (s) => {
|
|
20741
22932
|
if (!s.viewImage) {
|
|
20742
22933
|
throw new RoutingUnsupportedError("viewImage", this.cached?.kind ?? "unknown");
|
|
20743
22934
|
}
|
|
@@ -20745,7 +22936,7 @@ var RoutingSandboxSession = class {
|
|
|
20745
22936
|
});
|
|
20746
22937
|
}
|
|
20747
22938
|
async materializeEntry(args) {
|
|
20748
|
-
return this.dispatch("materializeEntry", async (s) => {
|
|
22939
|
+
return this.dispatch("materializeEntry", true, async (s) => {
|
|
20749
22940
|
if (!s.materializeEntry) {
|
|
20750
22941
|
throw new RoutingUnsupportedError("materializeEntry", this.cached?.kind ?? "unknown");
|
|
20751
22942
|
}
|
|
@@ -20771,13 +22962,12 @@ var RoutingSandboxSession = class {
|
|
|
20771
22962
|
* direct delegate returns undefined and every lazy turn would die at bind. Return a
|
|
20772
22963
|
* LAZY EDITOR PROXY instead: a non-null editor whose async ops resolve the active
|
|
20773
22964
|
* backend (establishing the box on first use, via `dispatch`) and delegate to its
|
|
20774
|
-
* 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. */
|
|
20775
22969
|
createEditor(runAs) {
|
|
20776
|
-
const
|
|
20777
|
-
if (eager) {
|
|
20778
|
-
return eager;
|
|
20779
|
-
}
|
|
20780
|
-
const op = (name) => (operation, context) => this.dispatch(`editor.${name}`, async (s) => {
|
|
22970
|
+
const op = (name) => (operation, context) => this.dispatch(`editor.${name}`, true, async (s) => {
|
|
20781
22971
|
const editor = s.createEditor?.(runAs);
|
|
20782
22972
|
if (!editor?.[name]) {
|
|
20783
22973
|
throw new RoutingUnsupportedError(`editor.${name}`, this.cached?.kind ?? "unknown");
|
|
@@ -20791,7 +22981,7 @@ var RoutingSandboxSession = class {
|
|
|
20791
22981
|
};
|
|
20792
22982
|
}
|
|
20793
22983
|
async resolveExposedPort(port) {
|
|
20794
|
-
return this.dispatch("resolveExposedPort", async (s) => {
|
|
22984
|
+
return this.dispatch("resolveExposedPort", false, async (s) => {
|
|
20795
22985
|
if (!s.resolveExposedPort) {
|
|
20796
22986
|
throw new RoutingUnsupportedError("resolveExposedPort", this.cached?.kind ?? "unknown");
|
|
20797
22987
|
}
|
|
@@ -20801,7 +22991,7 @@ var RoutingSandboxSession = class {
|
|
|
20801
22991
|
/** Serialize the active backend's session state. Used by the resume-by-id seam
|
|
20802
22992
|
* to fold the live box onto the lease. Dispatches to the active backend. */
|
|
20803
22993
|
async serializeSessionState() {
|
|
20804
|
-
return this.dispatch("serializeSessionState", async (s) => {
|
|
22994
|
+
return this.dispatch("serializeSessionState", false, async (s) => {
|
|
20805
22995
|
if (!s.serializeSessionState) {
|
|
20806
22996
|
return void 0;
|
|
20807
22997
|
}
|
|
@@ -20853,6 +23043,9 @@ function makeActiveBackendResolver(deps) {
|
|
|
20853
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"
|
|
20854
23044
|
);
|
|
20855
23045
|
}
|
|
23046
|
+
if (deps.resolveDefaultBackend) {
|
|
23047
|
+
return await deps.resolveDefaultBackend(pointer);
|
|
23048
|
+
}
|
|
20856
23049
|
return { session: deps.defaultBackend, sandboxId: null, kind: deps.defaultKind };
|
|
20857
23050
|
}
|
|
20858
23051
|
if (deps.pinnedSelfhosted && pointer.activeSandboxId === deps.pinnedSelfhosted.sandboxId && pointer.activeEpoch === deps.pinnedSelfhosted.epoch) {
|
|
@@ -21035,20 +23228,6 @@ function readWorkspaceArchiveFromEnvelopeSessionState(sessionState) {
|
|
|
21035
23228
|
return void 0;
|
|
21036
23229
|
}
|
|
21037
23230
|
}
|
|
21038
|
-
function readWorkspaceArchivePairFromEnvelopeSessionState(sessionState) {
|
|
21039
|
-
if (!sessionState || typeof sessionState !== "object") {
|
|
21040
|
-
return {};
|
|
21041
|
-
}
|
|
21042
|
-
const state = sessionState;
|
|
21043
|
-
const current = readWorkspaceArchiveFromEnvelopeSessionState(sessionState);
|
|
21044
|
-
const previous = readWorkspaceArchiveFromEnvelopeSessionState({
|
|
21045
|
-
workspaceArchive: state.workspaceArchivePrev
|
|
21046
|
-
});
|
|
21047
|
-
return {
|
|
21048
|
-
...current ? { current } : {},
|
|
21049
|
-
...previous ? { previous } : {}
|
|
21050
|
-
};
|
|
21051
|
-
}
|
|
21052
23231
|
var MODAL_SNAPSHOT_REF_PREFIXES = [
|
|
21053
23232
|
"MODAL_SANDBOX_FS_SNAPSHOT_V1\n",
|
|
21054
23233
|
"MODAL_SANDBOX_DIR_SNAPSHOT_V1\n"
|
|
@@ -21119,39 +23298,73 @@ async function deserializeSandboxSessionStateEnvelope(client, envelope) {
|
|
|
21119
23298
|
...state.exposedPorts ? { exposedPorts: structuredClone(state.exposedPorts) } : {}
|
|
21120
23299
|
});
|
|
21121
23300
|
}
|
|
21122
|
-
|
|
21123
|
-
|
|
21124
|
-
|
|
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;
|
|
21125
23310
|
}
|
|
21126
|
-
|
|
21127
|
-
|
|
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;
|
|
21128
23317
|
}
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
|
|
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;
|
|
21132
23322
|
}
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
|
|
21137
|
-
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
|
|
21143
|
-
|
|
21144
|
-
|
|
21145
|
-
|
|
21146
|
-
|
|
21147
|
-
|
|
21148
|
-
|
|
21149
|
-
|
|
21150
|
-
|
|
21151
|
-
|
|
21152
|
-
|
|
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);
|
|
21153
23367
|
}
|
|
21154
|
-
return goneMarkers.some((marker) => haystack.includes(marker));
|
|
21155
23368
|
}
|
|
21156
23369
|
function readInstanceId(session) {
|
|
21157
23370
|
const state = session.state ?? {};
|
|
@@ -21189,9 +23402,14 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
|
|
|
21189
23402
|
}
|
|
21190
23403
|
const createManifest = { environment };
|
|
21191
23404
|
const envelopeSessionState = envelope && typeof envelope === "object" ? envelope.sessionState : void 0;
|
|
21192
|
-
const
|
|
21193
|
-
const
|
|
21194
|
-
const
|
|
23405
|
+
const archiveState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState : void 0;
|
|
23406
|
+
const workspaceArchiveBase64 = archiveState?.workspaceArchive;
|
|
23407
|
+
const workspaceArchiveMetadata = archiveState?.workspaceArchiveMeta;
|
|
23408
|
+
const coldRestore = async (resumeFallbackState) => {
|
|
23409
|
+
const workspaceArchive = readVerifiedWorkspaceArchive(
|
|
23410
|
+
workspaceArchiveBase64,
|
|
23411
|
+
workspaceArchiveMetadata
|
|
23412
|
+
);
|
|
21195
23413
|
const createStarted = Date.now();
|
|
21196
23414
|
let restored;
|
|
21197
23415
|
try {
|
|
@@ -21218,56 +23436,68 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
|
|
|
21218
23436
|
}
|
|
21219
23437
|
}
|
|
21220
23438
|
let hydrationApplied = false;
|
|
21221
|
-
if (
|
|
23439
|
+
if (workspaceArchive) {
|
|
21222
23440
|
const hydrate = restored.hydrateWorkspace;
|
|
21223
|
-
if (typeof hydrate
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
21227
|
-
|
|
21228
|
-
|
|
21229
|
-
|
|
21230
|
-
|
|
21231
|
-
|
|
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) {
|
|
21232
23470
|
try {
|
|
21233
|
-
await
|
|
21234
|
-
|
|
21235
|
-
|
|
21236
|
-
|
|
21237
|
-
hydrated = true;
|
|
21238
|
-
break;
|
|
21239
|
-
} catch (hydrateError) {
|
|
21240
|
-
console.warn(
|
|
21241
|
-
`[sandbox] cold-restore failed to hydrate workspace from ${candidate.label} archive${candidate.label === "current" && workspaceArchives.previous ? "; trying previous archive" : ""}`,
|
|
21242
|
-
hydrateError
|
|
21243
|
-
);
|
|
23471
|
+
await opts.onSandboxCreated(established);
|
|
23472
|
+
} catch (createCallbackError) {
|
|
23473
|
+
await terminateCreatedSandbox(client, restored, hydratedState);
|
|
23474
|
+
throw createCallbackError;
|
|
21244
23475
|
}
|
|
21245
23476
|
}
|
|
21246
|
-
|
|
21247
|
-
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
|
|
21252
|
-
|
|
21253
|
-
if (hydratedInstanceId && hydratedInstanceId !== established.instanceId) {
|
|
21254
|
-
established = {
|
|
21255
|
-
client,
|
|
21256
|
-
session: restored,
|
|
21257
|
-
sessionState: hydratedState ?? resumeFallbackState,
|
|
21258
|
-
instanceId: hydratedInstanceId,
|
|
21259
|
-
backendId: client.backendId
|
|
21260
|
-
};
|
|
21261
|
-
if (opts.onSandboxCreated) {
|
|
21262
|
-
try {
|
|
21263
|
-
await opts.onSandboxCreated(established);
|
|
21264
|
-
} catch (createCallbackError) {
|
|
21265
|
-
await terminateCreatedSandbox(client, restored, hydratedState);
|
|
21266
|
-
throw createCallbackError;
|
|
21267
|
-
}
|
|
21268
|
-
}
|
|
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;
|
|
21269
23484
|
}
|
|
21270
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
|
+
);
|
|
21271
23501
|
}
|
|
21272
23502
|
restoredState = restored.state;
|
|
21273
23503
|
return {
|
|
@@ -21276,7 +23506,8 @@ async function establishSandboxSessionFromEnvelope(settings, envelope, opts) {
|
|
|
21276
23506
|
sessionState: restoredState ?? resumeFallbackState,
|
|
21277
23507
|
instanceId: readInstanceId(restored),
|
|
21278
23508
|
backendId: client.backendId,
|
|
21279
|
-
origin: hydrationApplied ? "restored" : "created"
|
|
23509
|
+
origin: hydrationApplied ? "restored" : "created",
|
|
23510
|
+
...workspaceArchive ? { restoredArchive: workspaceArchive.descriptor } : {}
|
|
21280
23511
|
};
|
|
21281
23512
|
};
|
|
21282
23513
|
const envelopeProviderState = envelopeSessionState && typeof envelopeSessionState === "object" ? envelopeSessionState.providerState : void 0;
|
|
@@ -21362,6 +23593,79 @@ async function serializeEstablishedSandboxEnvelope(established) {
|
|
|
21362
23593
|
return null;
|
|
21363
23594
|
}
|
|
21364
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
|
+
}
|
|
21365
23669
|
|
|
21366
23670
|
export {
|
|
21367
23671
|
localDirLazySkillSource,
|
|
@@ -21429,6 +23733,17 @@ export {
|
|
|
21429
23733
|
isSelfhostedProviderNotFoundError,
|
|
21430
23734
|
PROVIDER_REGISTRY,
|
|
21431
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,
|
|
21432
23747
|
selectBackend,
|
|
21433
23748
|
backendSupportsOs,
|
|
21434
23749
|
desktopCapableBackend,
|
|
@@ -21437,6 +23752,9 @@ export {
|
|
|
21437
23752
|
mintStreamToken,
|
|
21438
23753
|
verifyStreamToken,
|
|
21439
23754
|
StreamTokenPayload2 as StreamTokenPayload,
|
|
23755
|
+
parseExecBannerSessionId,
|
|
23756
|
+
parseExecBannerExitCode,
|
|
23757
|
+
isExecSessionLostBanner,
|
|
21440
23758
|
STREAM_PORT,
|
|
21441
23759
|
DISPLAY_STACK_TIMEOUT_MS,
|
|
21442
23760
|
DEFAULT_DESKTOP_GEOMETRY,
|
|
@@ -21481,6 +23799,9 @@ export {
|
|
|
21481
23799
|
uploadRecordingArtifact,
|
|
21482
23800
|
deleteRecordingArtifacts,
|
|
21483
23801
|
recordingStorageKey,
|
|
23802
|
+
TurnSandboxCommandCancelledError,
|
|
23803
|
+
createTurnToolCancellationController,
|
|
23804
|
+
wrapCapabilityToolsForTurnCancellation,
|
|
21484
23805
|
ChannelAValidationError,
|
|
21485
23806
|
ChannelAUnavailableError,
|
|
21486
23807
|
ChannelAConflictError,
|
|
@@ -21489,9 +23810,6 @@ export {
|
|
|
21489
23810
|
REPOSITORY_DISCOVERY_LIMIT,
|
|
21490
23811
|
SandboxChannelAService,
|
|
21491
23812
|
stripExecBanner2 as stripExecBanner,
|
|
21492
|
-
isExecSessionLostBanner,
|
|
21493
|
-
parseExecBannerSessionId,
|
|
21494
|
-
parseExecBannerExitCode,
|
|
21495
23813
|
assertSafeRelPath,
|
|
21496
23814
|
parsePorcelainV2,
|
|
21497
23815
|
parseNumstatZ,
|
|
@@ -21501,6 +23819,9 @@ export {
|
|
|
21501
23819
|
negotiateSelfhostedCapabilities,
|
|
21502
23820
|
MockAgentResponder,
|
|
21503
23821
|
RoutingUnsupportedError,
|
|
23822
|
+
RoutingBackendRecoveryRequiredError,
|
|
23823
|
+
RoutingMutationOutcomeUnknownError,
|
|
23824
|
+
RoutingRetainedProcessNotFoundError,
|
|
21504
23825
|
RoutingSandboxSession,
|
|
21505
23826
|
ActiveBackendUnresolvableError,
|
|
21506
23827
|
swapTargetEstablishability,
|
|
@@ -21513,10 +23834,15 @@ export {
|
|
|
21513
23834
|
decodeModalSnapshotId,
|
|
21514
23835
|
deletePriorPersistedSnapshot,
|
|
21515
23836
|
deserializeSandboxSessionStateEnvelope,
|
|
21516
|
-
|
|
23837
|
+
SandboxExecReadinessError,
|
|
23838
|
+
verifySandboxExecReadiness,
|
|
21517
23839
|
establishSandboxSessionFromEnvelope,
|
|
21518
23840
|
serializeEstablishedSandboxEnvelope,
|
|
23841
|
+
hasPersistableSandboxProviderIdentity,
|
|
23842
|
+
SandboxReplacementProviderStateError,
|
|
23843
|
+
requirePersistableReplacementSandboxEnvelope,
|
|
23844
|
+
serializeReplacementSandboxEnvelope,
|
|
21519
23845
|
collectSandboxEnvironment2 as collectSandboxEnvironment,
|
|
21520
23846
|
parseExposedPorts2 as parseExposedPorts
|
|
21521
23847
|
};
|
|
21522
|
-
//# sourceMappingURL=chunk-
|
|
23848
|
+
//# sourceMappingURL=chunk-JGUQSAG5.js.map
|