@opengeni/runtime 4.0.1 → 4.0.3-canary.36079201993001

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.
Files changed (48) hide show
  1. package/dist/{agent-RJQLKFIW.js → agent-ME6HT4DH.js} +2 -2
  2. package/dist/assets/codemode-client.json +1 -1
  3. package/dist/browser-action-json-schema.d.ts +4 -0
  4. package/dist/{chunk-J5AR5XAJ.js → chunk-GBVN7WSK.js} +377 -210
  5. package/dist/chunk-GBVN7WSK.js.map +1 -0
  6. package/dist/{chunk-ZG2YH25H.js → chunk-JAYHA6BF.js} +2 -2
  7. package/dist/{chunk-UNICCRW5.js → chunk-LZIXCZUV.js} +6 -3
  8. package/dist/{chunk-UNICCRW5.js.map → chunk-LZIXCZUV.js.map} +1 -1
  9. package/dist/{chunk-J6J3VYFW.js → chunk-OGG5NQLA.js} +597 -65
  10. package/dist/chunk-OGG5NQLA.js.map +1 -0
  11. package/dist/{chunk-SPT3PLMX.js → chunk-PRHMSM6C.js} +2 -2
  12. package/dist/context-compaction.d.ts +4 -2
  13. package/dist/index.d.ts +8 -6
  14. package/dist/index.js +9 -7
  15. package/dist/interaction-tools.d.ts +1 -1
  16. package/dist/{lazy-manifest-OEYEHNVY.js → lazy-manifest-GCGUFIWP.js} +3 -3
  17. package/dist/{localSources-TWHTEKGO.js → localSources-FDH65AFI.js} +4 -4
  18. package/dist/operational-instructions.d.ts +1 -1
  19. package/dist/sandbox/browser-control-client.d.ts +28 -1
  20. package/dist/sandbox/index.d.ts +2 -2
  21. package/dist/sandbox/index.js +6 -6
  22. package/dist/sandbox/providers/modal-command-router-wire.d.ts +8 -0
  23. package/dist/sandbox/providers/modal.d.ts +5 -6
  24. package/dist/workspace-tool-gateway.js +5 -5
  25. package/package.json +11 -11
  26. package/src/browser-action-json-schema.ts +10 -0
  27. package/src/bundled_artifact_skills/opengeni-documents/SKILL.md +4 -1
  28. package/src/bundled_artifact_skills/opengeni-documents/references/api.md +20 -0
  29. package/src/bundled_default_skills/opengeni-client/SKILL.md +4 -1
  30. package/src/bundled_default_skills/opengeni-client/references/discovery-and-autonomy.md +31 -0
  31. package/src/bundled_default_skills/opengeni-client/references/runtime-profile-and-verification.md +32 -0
  32. package/src/context-compaction.ts +19 -3
  33. package/src/index.ts +100 -36
  34. package/src/interaction-tools.ts +656 -48
  35. package/src/operational-instructions.ts +1 -1
  36. package/src/sandbox/browser-control-client.ts +241 -26
  37. package/src/sandbox/index.ts +3 -1
  38. package/src/sandbox/providers/modal-command-control.ts +7 -1
  39. package/src/sandbox/providers/modal-command-router-wire.ts +47 -6
  40. package/src/sandbox/providers/modal.ts +16 -24
  41. package/src/sandbox/routing/routing-session.ts +4 -2
  42. package/dist/chunk-J5AR5XAJ.js.map +0 -1
  43. package/dist/chunk-J6J3VYFW.js.map +0 -1
  44. /package/dist/{agent-RJQLKFIW.js.map → agent-ME6HT4DH.js.map} +0 -0
  45. /package/dist/{chunk-ZG2YH25H.js.map → chunk-JAYHA6BF.js.map} +0 -0
  46. /package/dist/{chunk-SPT3PLMX.js.map → chunk-PRHMSM6C.js.map} +0 -0
  47. /package/dist/{lazy-manifest-OEYEHNVY.js.map → lazy-manifest-GCGUFIWP.js.map} +0 -0
  48. /package/dist/{localSources-TWHTEKGO.js.map → localSources-FDH65AFI.js.map} +0 -0
@@ -156,7 +156,7 @@ Skills are reusable instructions supplied dynamically for the current session. W
156
156
 
157
157
  # Integration setup
158
158
 
159
- Use available integration tools directly. If the task needs an integration you cannot access, discover it with \`capability_catalog_search\` before declaring a setup blocker. For a suitable match with \`setup.nextAction\`, call \`capability_authorization_request\` with the returned capability ID and a brief task-specific rationale to show its Connect card in chat. Requesting the card does not need integration-management permission; the authenticated human must authorize setup. A card is for an integration required by the authorized design, including established or delegated choices. Resolve out-of-scope architecture choices before requesting setup. After setup, rediscover the tools and continue the task, verifying the access it needs. If access remains blocked, explain the specific blocker from the returned facts. If either setup tool is unavailable, report the missing setup path.
159
+ Use available integration tools directly. If access is missing, search \`capability_catalog_search\` first. For a suitable match with \`setup.nextAction\`, call \`capability_authorization_request\` with its ID and a task-specific rationale. If no match exists and the task needs a remote MCP whose exact HTTPS URL the user supplied or reliable documentation establishes, call \`custom_mcp_setup_request\` with its name, URL, and rationale. Never invent URLs or request credentials in chat. Showing either card does not need integration-management permission and grants no access; the authenticated human must authorize setup. A card is for an integration required by the authorized design, including established or delegated choices. Resolve out-of-scope architecture choices before requesting setup. After setup, rediscover tools and verify access. If blocked or a setup tool is unavailable, explain the specific gap.
160
160
 
161
161
  # Session coordination
162
162
 
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { posix as posixPath } from "node:path";
3
+ import { parseBrowserFrameMetadata, type BrowserFrameMetadata } from "@opengeni/sdk";
3
4
  import {
4
5
  BROWSER_CONTROL_MAX_JSON_BYTES,
5
6
  BROWSER_CONTROL_PORT,
@@ -14,6 +15,8 @@ import {
14
15
  BrowserDownloadExportReceipt,
15
16
  BrowserDownloadExportRequest,
16
17
  BrowserDownloadListResponse,
18
+ BrowserDomReadRequest,
19
+ BrowserDomReadResponse,
17
20
  BrowserExternalAuthCommand,
18
21
  BrowserExternalAuthResult,
19
22
  BrowserObservation,
@@ -21,6 +24,7 @@ import {
21
24
  BrowserProtectedAuthFillReceipt,
22
25
  BrowserRevisionMaterialization,
23
26
  BrowserTarget,
27
+ BrowserTargetState,
24
28
  BrowserWorkspaceFileStageRequest,
25
29
  BrowserWorkspaceFileStageResponse,
26
30
  ComputerActionCommand,
@@ -40,6 +44,8 @@ import {
40
44
  type BrowserDownloadExportReceipt as BrowserDownloadExportReceiptValue,
41
45
  type BrowserDownloadExportRequest as BrowserDownloadExportRequestValue,
42
46
  type BrowserDownloadListResponse as BrowserDownloadListResponseValue,
47
+ type BrowserDomReadRequest as BrowserDomReadRequestValue,
48
+ type BrowserDomReadResponse as BrowserDomReadResponseValue,
43
49
  type BrowserExternalAuthCommand as BrowserExternalAuthCommandValue,
44
50
  type BrowserExternalAuthResult as BrowserExternalAuthResultValue,
45
51
  type BrowserDiagnosticKind,
@@ -48,6 +54,7 @@ import {
48
54
  type BrowserProtectedAuthFillReceipt as BrowserProtectedAuthFillReceiptValue,
49
55
  type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue,
50
56
  type BrowserTarget as BrowserTargetValue,
57
+ type BrowserTargetState as BrowserTargetStateValue,
51
58
  type BrowserWorkspaceFileStageRequest as BrowserWorkspaceFileStageRequestValue,
52
59
  type BrowserWorkspaceFileStageResponse as BrowserWorkspaceFileStageResponseValue,
53
60
  type ComputerActionCommand as ComputerActionCommandValue,
@@ -165,6 +172,35 @@ export type ComputerControlFrame = {
165
172
  metadata: ComputerControlFrameMetadata;
166
173
  };
167
174
 
175
+ export type BrowserControlFrame = {
176
+ data: Uint8Array;
177
+ mediaType: "image/jpeg" | "image/png";
178
+ metadataHeader: string;
179
+ metadata: BrowserFrameMetadata;
180
+ };
181
+
182
+ export type BrowserControlScreenshotOptions = {
183
+ fullPage?: boolean;
184
+ format?: "jpeg" | "png";
185
+ quality?: number;
186
+ };
187
+
188
+ type ExpectedBrowserFrameEvidence = {
189
+ browserSessionId: string;
190
+ controllerGeneration: string;
191
+ targetId: string;
192
+ };
193
+
194
+ type FrameRequest = {
195
+ method: "GET";
196
+ path: string;
197
+ token: string;
198
+ timeoutMs?: number;
199
+ } & (
200
+ | { surface: "browser"; expectedFrameEvidence: ExpectedBrowserFrameEvidence }
201
+ | { surface: "computer"; expectedFrameEvidence: ExpectedComputerFrameEvidence }
202
+ );
203
+
168
204
  export type ComputerControlFrameMetadata = {
169
205
  frameId: string;
170
206
  computerSessionId: string;
@@ -947,7 +983,17 @@ export class BrowserControlClient {
947
983
  expectedFrameEvidence: ExpectedComputerFrameEvidence;
948
984
  timeoutMs?: number;
949
985
  }): Promise<ComputerControlFrame> {
950
- return await this.requestBytes(input);
986
+ return (await this.requestBytes({ ...input, surface: "computer" })) as ComputerControlFrame;
987
+ }
988
+
989
+ async requestBrowserBytesForSession(input: {
990
+ method: "GET";
991
+ path: string;
992
+ token: string;
993
+ expectedFrameEvidence: ExpectedBrowserFrameEvidence;
994
+ timeoutMs?: number;
995
+ }): Promise<BrowserControlFrame> {
996
+ return (await this.requestBytes({ ...input, surface: "browser" })) as BrowserControlFrame;
951
997
  }
952
998
 
953
999
  private async requestJson(
@@ -1124,15 +1170,9 @@ export class BrowserControlClient {
1124
1170
  }
1125
1171
 
1126
1172
  private async requestBytes(
1127
- input: {
1128
- method: "GET";
1129
- path: string;
1130
- token: string;
1131
- expectedFrameEvidence: ExpectedComputerFrameEvidence;
1132
- timeoutMs?: number;
1133
- },
1173
+ input: FrameRequest,
1134
1174
  retryNativeEndpoint = true,
1135
- ): Promise<ComputerControlFrame> {
1175
+ ): Promise<ComputerControlFrame | BrowserControlFrame> {
1136
1176
  const requireHostFetch = this.session.requireHostFetchController === true;
1137
1177
  if (this.session.resolveExposedPort && !this.session.ensureBrowserControl) {
1138
1178
  try {
@@ -1247,10 +1287,10 @@ export class BrowserControlClient {
1247
1287
  const data = await readBytes(
1248
1288
  this.session,
1249
1289
  responsePath,
1250
- COMPUTER_SCREENSHOT_MAX_BYTES,
1290
+ input.surface === "browser" ? 24 * 1024 * 1024 : COMPUTER_SCREENSHOT_MAX_BYTES,
1251
1291
  timeoutMs,
1252
1292
  );
1253
- return computerControlFrame(data, headers, input.expectedFrameEvidence);
1293
+ return controllerImageFrame(data, headers, input);
1254
1294
  } catch (error) {
1255
1295
  if (
1256
1296
  error instanceof BrowserControlRequestError ||
@@ -1432,6 +1472,104 @@ export class BrowserControlSessionClient {
1432
1472
  );
1433
1473
  }
1434
1474
 
1475
+ async targetState(targetId: string): Promise<BrowserTargetStateValue> {
1476
+ let state: BrowserTargetStateValue;
1477
+ try {
1478
+ state = BrowserTargetState.parse(
1479
+ await this.parent.requestForSession({
1480
+ method: "GET",
1481
+ path: this.targetPath(targetId, "state"),
1482
+ token: this.viewToken,
1483
+ }),
1484
+ );
1485
+ } catch (error) {
1486
+ // Active Connected Machines can run an older browserd during an API rollout.
1487
+ // Only its exact unknown-route response permits the existing observation path.
1488
+ // A missing session or target must keep its original error.
1489
+ if (
1490
+ !(error instanceof BrowserControlRequestError) ||
1491
+ error.status !== 404 ||
1492
+ error.error.code !== "resource_not_found" ||
1493
+ error.error.message !== "route not found"
1494
+ ) {
1495
+ throw error;
1496
+ }
1497
+ const observation = await this.observe(targetId);
1498
+ state = BrowserTargetState.parse({
1499
+ browserSessionId: observation.browserSessionId,
1500
+ controllerGeneration: observation.target.controllerGeneration,
1501
+ targetId: observation.target.id,
1502
+ targetGeneration: observation.target.targetGeneration,
1503
+ documentGeneration: observation.target.documentGeneration,
1504
+ frameId: observation.frameId,
1505
+ });
1506
+ }
1507
+ if (
1508
+ state.browserSessionId !== this.reference.browserSessionId ||
1509
+ state.controllerGeneration !== this.reference.controllerGeneration ||
1510
+ state.targetId !== targetId
1511
+ ) {
1512
+ throw new BrowserControlProtocolError("browser target state belongs to another binding");
1513
+ }
1514
+ return state;
1515
+ }
1516
+
1517
+ async readDom(
1518
+ targetId: string,
1519
+ requestInput: BrowserDomReadRequestValue,
1520
+ ): Promise<BrowserDomReadResponseValue> {
1521
+ const request = BrowserDomReadRequest.parse(requestInput);
1522
+ const result = BrowserDomReadResponse.parse(
1523
+ await this.parent.requestForSession({
1524
+ method: "POST",
1525
+ path: this.targetPath(targetId, "dom-read"),
1526
+ token: this.viewToken,
1527
+ body: request,
1528
+ }),
1529
+ );
1530
+ if (
1531
+ result.browserSessionId !== this.reference.browserSessionId ||
1532
+ result.controllerGeneration !== this.reference.controllerGeneration ||
1533
+ result.targetId !== targetId ||
1534
+ result.targetGeneration !== request.expectedTargetGeneration ||
1535
+ result.documentGeneration !== request.expectedDocumentGeneration ||
1536
+ result.frameId !== request.expectedFrameId ||
1537
+ result.kind !== request.kind
1538
+ ) {
1539
+ throw new BrowserControlProtocolError("browser DOM read belongs to another binding");
1540
+ }
1541
+ return result;
1542
+ }
1543
+
1544
+ async capture(
1545
+ targetId: string,
1546
+ options: BrowserControlScreenshotOptions = {},
1547
+ ): Promise<BrowserControlFrame> {
1548
+ const query = new URLSearchParams({
1549
+ format: options.format ?? "jpeg",
1550
+ quality: String(boundedInteger(options.quality ?? 55, 1, 100, "browser screenshot quality")),
1551
+ });
1552
+ if (options.format !== undefined && options.format !== "jpeg" && options.format !== "png") {
1553
+ throw new RangeError("browser screenshot format is invalid");
1554
+ }
1555
+ if (options.fullPage !== undefined) {
1556
+ if (typeof options.fullPage !== "boolean") {
1557
+ throw new RangeError("browser screenshot fullPage is invalid");
1558
+ }
1559
+ query.set("fullPage", String(options.fullPage));
1560
+ }
1561
+ return await this.parent.requestBrowserBytesForSession({
1562
+ method: "GET",
1563
+ path: this.targetPath(targetId, `screenshot?${query}`),
1564
+ token: this.viewToken,
1565
+ expectedFrameEvidence: {
1566
+ browserSessionId: this.reference.browserSessionId,
1567
+ controllerGeneration: this.reference.controllerGeneration,
1568
+ targetId: requireOpaqueId(targetId, "browser target id"),
1569
+ },
1570
+ });
1571
+ }
1572
+
1435
1573
  async readClipboard(): Promise<BrowserClipboardValue> {
1436
1574
  return BrowserClipboard.parse(
1437
1575
  await this.parent.requestForSession({
@@ -1840,15 +1978,9 @@ async function requestExposedController(
1840
1978
 
1841
1979
  async function requestExposedControllerBytes(
1842
1980
  endpoint: ExposedPortEndpoint,
1843
- input: {
1844
- method: "GET";
1845
- path: string;
1846
- token: string;
1847
- expectedFrameEvidence: ExpectedComputerFrameEvidence;
1848
- timeoutMs?: number;
1849
- },
1981
+ input: FrameRequest,
1850
1982
  defaultTimeoutMs: number,
1851
- ): Promise<ComputerControlFrame> {
1983
+ ): Promise<ComputerControlFrame | BrowserControlFrame> {
1852
1984
  const token = requireToken(input.token, "browser controller token");
1853
1985
  const timeoutMs = boundedTimeout(input.timeoutMs ?? defaultTimeoutMs);
1854
1986
  const streamUrl = exposedControllerUrl(endpoint, input.path);
@@ -1872,12 +2004,91 @@ async function requestExposedControllerBytes(
1872
2004
  throw new BrowserControlProtocolError("browser controller image response is invalid");
1873
2005
  }
1874
2006
  const declaredLength = Number(response.headers.get("content-length"));
1875
- if (Number.isFinite(declaredLength) && declaredLength > COMPUTER_SCREENSHOT_MAX_BYTES) {
1876
- await response.body?.cancel("computer frame exceeds its byte bound").catch(() => undefined);
1877
- throw new RangeError("computer frame exceeds its byte bound");
2007
+ const maxBytes = input.surface === "browser" ? 24 * 1024 * 1024 : COMPUTER_SCREENSHOT_MAX_BYTES;
2008
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
2009
+ await response.body?.cancel("frame exceeds its byte bound").catch(() => undefined);
2010
+ throw new RangeError("frame exceeds its byte bound");
2011
+ }
2012
+ const data = await readBoundedFrameResponse(response, maxBytes);
2013
+ return controllerImageFrame(data, response.headers, input);
2014
+ }
2015
+
2016
+ async function readBoundedFrameResponse(response: Response, maxBytes: number): Promise<Uint8Array> {
2017
+ if (!response.body) throw new BrowserControlProtocolError("browser controller image is empty");
2018
+ const reader = response.body.getReader();
2019
+ const chunks: Uint8Array[] = [];
2020
+ let size = 0;
2021
+ try {
2022
+ for (;;) {
2023
+ const { done, value } = await reader.read();
2024
+ if (done) break;
2025
+ size += value.byteLength;
2026
+ if (size > maxBytes) {
2027
+ await reader.cancel("frame exceeds its byte bound").catch(() => undefined);
2028
+ throw new RangeError("frame exceeds its byte bound");
2029
+ }
2030
+ chunks.push(value);
2031
+ }
2032
+ } finally {
2033
+ reader.releaseLock();
1878
2034
  }
1879
- const data = new Uint8Array(await response.arrayBuffer());
1880
- return computerControlFrame(data, response.headers, input.expectedFrameEvidence);
2035
+ const data = new Uint8Array(size);
2036
+ let offset = 0;
2037
+ for (const chunk of chunks) {
2038
+ data.set(chunk, offset);
2039
+ offset += chunk.byteLength;
2040
+ }
2041
+ return data;
2042
+ }
2043
+
2044
+ function controllerImageFrame(
2045
+ data: Uint8Array,
2046
+ headers: { get(name: string): string | null | undefined },
2047
+ input: FrameRequest,
2048
+ ): ComputerControlFrame | BrowserControlFrame {
2049
+ return input.surface === "browser"
2050
+ ? browserControlFrame(data, headers, input.expectedFrameEvidence)
2051
+ : computerControlFrame(data, headers, input.expectedFrameEvidence);
2052
+ }
2053
+
2054
+ function browserControlFrame(
2055
+ data: Uint8Array,
2056
+ headers: { get(name: string): string | null | undefined },
2057
+ expected: ExpectedBrowserFrameEvidence,
2058
+ ): BrowserControlFrame {
2059
+ if (data.byteLength < 1 || data.byteLength > 24 * 1024 * 1024) {
2060
+ throw new RangeError("browser frame exceeds its byte bound");
2061
+ }
2062
+ const get = (name: string): string | null => headers.get(name.toLowerCase()) ?? null;
2063
+ const mediaType = get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
2064
+ if (mediaType !== "image/jpeg" && mediaType !== "image/png") {
2065
+ throw new BrowserControlProtocolError("browser frame media type is invalid");
2066
+ }
2067
+ const metadataHeader = get("x-opengeni-browser-frame");
2068
+ if (
2069
+ !metadataHeader ||
2070
+ metadataHeader.length > 64 * 1024 ||
2071
+ !/^[A-Za-z0-9_-]+$/u.test(metadataHeader)
2072
+ ) {
2073
+ throw new BrowserControlProtocolError("browser frame metadata is invalid");
2074
+ }
2075
+ let metadata: BrowserFrameMetadata;
2076
+ try {
2077
+ metadata = parseBrowserFrameMetadata(
2078
+ JSON.parse(Buffer.from(metadataHeader, "base64url").toString("utf8")),
2079
+ );
2080
+ } catch {
2081
+ throw new BrowserControlProtocolError("browser frame metadata is invalid");
2082
+ }
2083
+ if (
2084
+ metadata.browserSessionId !== expected.browserSessionId ||
2085
+ metadata.controllerGeneration !== expected.controllerGeneration ||
2086
+ metadata.targetId !== expected.targetId ||
2087
+ metadata.mediaType !== mediaType
2088
+ ) {
2089
+ throw new BrowserControlProtocolError("browser frame evidence does not match its request");
2090
+ }
2091
+ return { data, mediaType, metadataHeader, metadata };
1881
2092
  }
1882
2093
 
1883
2094
  function computerControlFrame(
@@ -2764,7 +2975,11 @@ async function readBytes(
2764
2975
  timeoutMs: number,
2765
2976
  ): Promise<Uint8Array> {
2766
2977
  const privatePath = absolutePrivatePath(path, "browser private binary response path");
2767
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > COMPUTER_SCREENSHOT_MAX_BYTES) {
2978
+ if (
2979
+ !Number.isSafeInteger(maxBytes) ||
2980
+ maxBytes < 1 ||
2981
+ maxBytes > Math.max(24 * 1024 * 1024, COMPUTER_SCREENSHOT_MAX_BYTES)
2982
+ ) {
2768
2983
  throw new RangeError("browser private binary response limit is invalid");
2769
2984
  }
2770
2985
  const sizeMarker = `OPENGENI_BROWSER_PRIVATE_SIZE_${randomUUID()}`;
@@ -2783,7 +2998,7 @@ async function readBytes(
2783
2998
  "browser private binary response size",
2784
2999
  );
2785
3000
  if (size < 1 || size > maxBytes) {
2786
- throw new RangeError("computer frame exceeds its byte bound");
3001
+ throw new RangeError("frame exceeds its byte bound");
2787
3002
  }
2788
3003
  const chunks: Buffer[] = [];
2789
3004
  for (let offset = 0; offset < size; offset += PRIVATE_READ_CHUNK_BYTES) {
@@ -175,7 +175,7 @@ export {
175
175
  ensureModalRegistryImage,
176
176
  deleteModalCheckpointSnapshot,
177
177
  inspectModalSandboxLifecycle,
178
- isModalTaskExecStartDnsResolutionError,
178
+ isModalTaskExecStartPreDispatchUnavailableError,
179
179
  modalSessionMatchesCheckpointProviderBinding,
180
180
  modalSandboxAttributionEnvironment,
181
181
  modalSandboxAttributionTags,
@@ -303,6 +303,8 @@ export {
303
303
  provisionBrowserControlClient,
304
304
  validateComputerControlFrameEvidence,
305
305
  type BrowserControlPlacementSession,
306
+ type BrowserControlScreenshotOptions,
307
+ type BrowserControlFrame,
306
308
  type BrowserStateDownloadGrant,
307
309
  type BrowserStateUploadGrant,
308
310
  type BrowserViewGrant,
@@ -20,6 +20,7 @@ import {
20
20
  } from "./modal-legacy-command-control";
21
21
  import {
22
22
  ModalCommandRouterWire,
23
+ ModalCommandStartPreDispatchUnavailableError,
23
24
  ModalCommandStartRejectedError,
24
25
  } from "./modal-command-router-wire";
25
26
 
@@ -236,7 +237,12 @@ export class ModalCommandControl {
236
237
  // A client-chosen router id remains the only possible invocation. The
237
238
  // supervisor is idle, so an ambiguous launch never ran user code. Retain
238
239
  // the descriptor and reconcile that id; do not replay the start.
239
- if (!supervision || error instanceof ModalCommandStartRejectedError) throw error;
240
+ if (
241
+ !supervision ||
242
+ error instanceof ModalCommandStartRejectedError ||
243
+ error instanceof ModalCommandStartPreDispatchUnavailableError
244
+ )
245
+ throw error;
240
246
  }
241
247
  return command;
242
248
  }
@@ -47,6 +47,35 @@ export class ModalCommandStartRejectedError extends ProviderCommandStartRejected
47
47
  }
48
48
  }
49
49
 
50
+ /** Only a local channel-readiness failure BEFORE TaskExecStart is issued can
51
+ * prove that start was never dispatched. RPC status/details, even if they look
52
+ * like a DNS resolver error, can originate from an accepting server. */
53
+ export class ModalCommandStartPreDispatchUnavailableError extends Error {
54
+ private constructor(cause: Error) {
55
+ super("Modal command router was not ready before Start dispatch", { cause });
56
+ this.name = "ModalCommandStartPreDispatchUnavailableError";
57
+ }
58
+
59
+ static async ensureReady(client: Client, signal?: AbortSignal): Promise<void> {
60
+ signal?.throwIfAborted();
61
+ await new Promise<void>((resolve, reject) => {
62
+ let settled = false;
63
+ const finish = (error?: Error) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ signal?.removeEventListener("abort", abort);
67
+ if (signal?.aborted) reject(signal.reason);
68
+ else if (error) reject(new ModalCommandStartPreDispatchUnavailableError(error));
69
+ else resolve();
70
+ };
71
+ const abort = () => finish();
72
+ signal?.addEventListener("abort", abort, { once: true });
73
+ client.waitForReady(Date.now() + 5_000, finish);
74
+ if (signal?.aborted) abort();
75
+ });
76
+ }
77
+ }
78
+
50
79
  export type ModalRouterIdentity = { taskId: string; execId: string };
51
80
  export type ModalRouterAccess = { url: string; jwt: string };
52
81
  export type ModalRouterStart = ModalRouterIdentity & {
@@ -76,16 +105,21 @@ export class ModalCommandRouterWire {
76
105
  private readonly client: Client;
77
106
  private readonly metadata: Metadata;
78
107
  private closed = false;
79
-
80
108
  constructor(access: ModalRouterAccess, trustedRoots?: Buffer) {
81
109
  const url = new URL(access.url);
82
110
  if (url.protocol !== "https:" || url.username || url.password || !access.jwt)
83
111
  throw new Error("Modal command router requires authenticated TLS");
84
- this.client = new Client(url.host, credentials.createSsl(trustedRoots), {
85
- "grpc.max_receive_message_length": maxWireBytes,
86
- "grpc.max_send_message_length": maxWireBytes,
87
- "grpc.enable_retries": 0,
88
- });
112
+ // URL.host elides HTTPS's default :443; use the actual normalized gRPC
113
+ // target for both the resolver and the authenticated TLS transport.
114
+ this.client = new Client(
115
+ `${url.hostname}:${url.port || "443"}`,
116
+ credentials.createSsl(trustedRoots),
117
+ {
118
+ "grpc.max_receive_message_length": maxWireBytes,
119
+ "grpc.max_send_message_length": maxWireBytes,
120
+ "grpc.enable_retries": 0,
121
+ },
122
+ );
89
123
  this.metadata = new Metadata();
90
124
  this.metadata.set("authorization", `Bearer ${access.jwt}`);
91
125
  }
@@ -127,6 +161,13 @@ export class ModalCommandRouterWire {
127
161
  }
128
162
 
129
163
  async start(request: ModalRouterStart, signal?: AbortSignal): Promise<void> {
164
+ if (this.closed) throw new Error("Modal command router is closed");
165
+ try {
166
+ await ModalCommandStartPreDispatchUnavailableError.ensureReady(this.client, signal);
167
+ } catch (error) {
168
+ if (this.closed) throw new Error("Modal command router is closed", { cause: error });
169
+ throw error;
170
+ }
130
171
  try {
131
172
  await this.unary(
132
173
  "TaskExecStart",
@@ -15,6 +15,8 @@ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
15
15
  import { SandboxChannelAService, type ChannelASession } from "../channel-a";
16
16
  import { installModalCommandSession } from "./modal-command-session";
17
17
  import { ModalCommandControl } from "./modal-command-control";
18
+ import { ModalCommandStartPreDispatchUnavailableError } from "./modal-command-router-wire";
19
+ import { isRoutingMutationOutcomeUnknownError } from "../routing/routing-session";
18
20
  import type { ModalClient } from "modal";
19
21
  import { ModalProcessObservationUnavailableError, SandboxConfigError } from "../errors";
20
22
  export { ModalProcessObservationUnavailableError } from "../errors";
@@ -207,9 +209,6 @@ const MODAL_EXEC_STDIN_WRITE_PATH =
207
209
  "/modal.task_command_router.TaskCommandRouter/TaskExecStdinWrite";
208
210
  const MODAL_EXEC_ALREADY_COMPLETED_DETAILS =
209
211
  /^Exec has already completed; stdin is no longer accepting writes(?: \(Error code: [A-Z0-9]+\))?$/;
210
- const MODAL_TASK_EXEC_START_PATH = "/modal.task_command_router.TaskCommandRouter/TaskExecStart";
211
- const MODAL_TASK_EXEC_START_DNS_RESOLUTION_DETAILS =
212
- /^Name resolution failed for target dns:task-[a-z0-9]+\.w\.modal\.host:443$/;
213
212
  const MODAL_TASK_EXEC_START_ERROR_MAX_DEPTH = 8;
214
213
  const MODAL_TASK_EXEC_START_ERROR_MAX_NODES = 64;
215
214
  const MODAL_TASK_EXEC_START_ERROR_MAX_AGGREGATE_ERRORS = 32;
@@ -234,24 +233,13 @@ function hasContradictoryModalHttpStatus(record: Record<string, unknown>): boole
234
233
  return values.some((value) => modalHttpStatus(value) !== null);
235
234
  }
236
235
 
237
- function isModalTaskExecStartDnsResolutionLeaf(record: Record<string, unknown>): boolean {
238
- return (
239
- record.name === "ClientError" &&
240
- record.path === MODAL_TASK_EXEC_START_PATH &&
241
- (record.code === 14 || record.code === "UNAVAILABLE") &&
242
- typeof record.details === "string" &&
243
- MODAL_TASK_EXEC_START_DNS_RESOLUTION_DETAILS.test(record.details)
244
- );
245
- }
246
-
247
236
  /**
248
- * Modal exhausted its own retries before failing to resolve the exact command
249
- * router DNS name, so TaskExecStart never connected and replay is safe. Keep
250
- * this fail-closed across Agents SDK wrappers: every reachable structural leaf
251
- * must be that exact ClientError with no HTTP status metadata, and any incomplete
252
- * or mixed graph is rejected.
237
+ * Only the native client's pre-dispatch readiness gate proves Start was not
238
+ * sent. Server-controlled gRPC/SDK status and DNS-looking details prove nothing.
239
+ * Any retained or outcome-unknown routing boundary vetoes recovery even when
240
+ * it contains a genuine pre-dispatch error among its causes.
253
241
  */
254
- export function isModalTaskExecStartDnsResolutionError(error: unknown): boolean {
242
+ export function isModalTaskExecStartPreDispatchUnavailableError(error: unknown): boolean {
255
243
  const pending: Array<{ depth: number; value: unknown }> = [{ depth: 0, value: error }];
256
244
  const seen = new WeakSet<object>();
257
245
  let inspected = 0;
@@ -270,8 +258,16 @@ export function isModalTaskExecStartDnsResolutionError(error: unknown): boolean
270
258
  let nested: unknown[];
271
259
  try {
272
260
  const record = current.value as Record<string, unknown>;
261
+ if (isRoutingMutationOutcomeUnknownError(current.value)) return false;
273
262
  if (hasContradictoryModalHttpStatus(record)) return false;
274
263
 
264
+ // This instance is created only by the client's readiness gate before
265
+ // Start dispatch; an RPC's own status or details never enters this path.
266
+ if (current.value instanceof ModalCommandStartPreDispatchUnavailableError) {
267
+ matchingLeaves += 1;
268
+ continue;
269
+ }
270
+
275
271
  nested = [];
276
272
  for (const key of ["cause", "error"] as const) {
277
273
  const value = record[key];
@@ -290,11 +286,7 @@ export function isModalTaskExecStartDnsResolutionError(error: unknown): boolean
290
286
  nested.push(...errors);
291
287
  }
292
288
 
293
- if (nested.length === 0) {
294
- if (!isModalTaskExecStartDnsResolutionLeaf(record)) return false;
295
- matchingLeaves += 1;
296
- continue;
297
- }
289
+ if (nested.length === 0) return false;
298
290
  } catch {
299
291
  return false;
300
292
  }
@@ -28,6 +28,7 @@
28
28
  // `@opengeni/db`.
29
29
 
30
30
  import type { ExposedPortEndpoint } from "../stream-port";
31
+ import { ModalCommandStartPreDispatchUnavailableError } from "../providers/modal-command-router-wire";
31
32
  import { isDeepStrictEqual } from "node:util";
32
33
  import {
33
34
  withProviderCommandHandle,
@@ -1687,7 +1688,8 @@ export class RoutingSandboxSession implements RoutableBackendSession {
1687
1688
  } catch (error) {
1688
1689
  if (reservedProcess) {
1689
1690
  if (
1690
- error instanceof ProviderCommandStartRejectedError &&
1691
+ (error instanceof ProviderCommandStartRejectedError ||
1692
+ error instanceof ModalCommandStartPreDispatchUnavailableError) &&
1691
1693
  reservedProcess.providerCommand?.kind === "modal-router-v1"
1692
1694
  ) {
1693
1695
  const persistence = this.deps.providerCommandPersistence?.(reservedProcess);
@@ -1697,7 +1699,7 @@ export class RoutingSandboxSession implements RoutableBackendSession {
1697
1699
  } catch (cause) {
1698
1700
  throw new RoutingMutationOutcomeUnknownError(
1699
1701
  op,
1700
- "Never-started provider rejection could not settle its exact reservation",
1702
+ "Never-started provider call could not settle its exact reservation",
1701
1703
  { cause, retainedProcess: reservedProcess },
1702
1704
  );
1703
1705
  }