@opengeni/runtime 3.2.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/agent-LJT5NPWT.js +9 -0
  2. package/dist/assets/codemode-client.json +1 -1
  3. package/dist/browser-current-tab.d.ts +46 -0
  4. package/dist/browser-model-text.d.ts +126 -0
  5. package/dist/{chunk-2AUDDDO2.js → chunk-3DFH3ZHU.js} +784 -121
  6. package/dist/chunk-3DFH3ZHU.js.map +1 -0
  7. package/dist/{chunk-HJSIPTFA.js → chunk-4XDJ4TJA.js} +2 -2
  8. package/dist/{chunk-XQ23VCZ4.js → chunk-6FQ6KEII.js} +3 -3
  9. package/dist/{chunk-7LJ7KDPI.js → chunk-ERD4ICVB.js} +4 -3
  10. package/dist/{chunk-462CH77G.js → chunk-ETTCGPBT.js} +400 -9
  11. package/dist/chunk-ETTCGPBT.js.map +1 -0
  12. package/dist/{chunk-7OUG7WBV.js → chunk-GWRBALGQ.js} +181 -13
  13. package/dist/chunk-GWRBALGQ.js.map +1 -0
  14. package/dist/{chunk-YG26XE7B.js → chunk-JYDG2A7H.js} +2 -2
  15. package/dist/{chunk-RSHQSLEJ.js → chunk-KIL4FGX5.js} +3 -3
  16. package/dist/{chunk-NWS4HPB7.js → chunk-MIAL5I4O.js} +2 -2
  17. package/dist/{chunk-Z5U6WE22.js → chunk-PRZPGRIK.js} +2 -2
  18. package/dist/{chunk-KCPW4K7L.js → chunk-PZQUUBEB.js} +2 -2
  19. package/dist/{chunk-5WJOPANS.js → chunk-Y5THZH5N.js} +7 -7
  20. package/dist/codex-tool-search.d.ts +9 -0
  21. package/dist/gmail-rest-mcp.js +8 -8
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.js +14 -12
  24. package/dist/interaction-tool-json-schema.d.ts +27 -0
  25. package/dist/{lazy-manifest-AJ6XPJ4F.js → lazy-manifest-BLZ3UZLX.js} +4 -4
  26. package/dist/lazy-tool-transport.d.ts +10 -0
  27. package/dist/{localSources-SSIEH5F5.js → localSources-I6SIQ6CX.js} +6 -6
  28. package/dist/model-call.js +3 -3
  29. package/dist/sandbox/browser-control-client.d.ts +87 -0
  30. package/dist/sandbox/browser-control-http.d.ts +33 -0
  31. package/dist/sandbox/index.d.ts +1 -1
  32. package/dist/sandbox/index.js +7 -7
  33. package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
  34. package/dist/sandbox/selfhosted/session.d.ts +22 -0
  35. package/dist/slack-rest-mcp.js +8 -8
  36. package/dist/workspace-tool-gateway.js +12 -12
  37. package/package.json +14 -14
  38. package/src/browser-current-tab.ts +139 -0
  39. package/src/browser-model-text.ts +378 -0
  40. package/src/bundled_site_skills/opengeni-sites/SKILL.md +46 -12
  41. package/src/codex-tool-search.ts +53 -0
  42. package/src/index.ts +39 -1
  43. package/src/interaction-tool-json-schema.ts +281 -0
  44. package/src/interaction-tools.ts +425 -235
  45. package/src/lazy-tool-transport.ts +229 -10
  46. package/src/model-input.ts +12 -1
  47. package/src/sandbox/browser-control-client.ts +425 -0
  48. package/src/sandbox/browser-control-http.ts +32 -0
  49. package/src/sandbox/index.ts +4 -0
  50. package/src/sandbox/routing/backend-resolver.ts +2 -0
  51. package/src/sandbox/selfhosted/session.ts +154 -0
  52. package/dist/agent-CTPLA3HP.js +0 -9
  53. package/dist/browser-action-json-schema.d.ts +0 -4
  54. package/dist/chunk-2AUDDDO2.js.map +0 -1
  55. package/dist/chunk-462CH77G.js.map +0 -1
  56. package/dist/chunk-7OUG7WBV.js.map +0 -1
  57. package/src/browser-action-json-schema.ts +0 -10
  58. /package/dist/{agent-CTPLA3HP.js.map → agent-LJT5NPWT.js.map} +0 -0
  59. /package/dist/{chunk-HJSIPTFA.js.map → chunk-4XDJ4TJA.js.map} +0 -0
  60. /package/dist/{chunk-XQ23VCZ4.js.map → chunk-6FQ6KEII.js.map} +0 -0
  61. /package/dist/{chunk-7LJ7KDPI.js.map → chunk-ERD4ICVB.js.map} +0 -0
  62. /package/dist/{chunk-YG26XE7B.js.map → chunk-JYDG2A7H.js.map} +0 -0
  63. /package/dist/{chunk-RSHQSLEJ.js.map → chunk-KIL4FGX5.js.map} +0 -0
  64. /package/dist/{chunk-NWS4HPB7.js.map → chunk-MIAL5I4O.js.map} +0 -0
  65. /package/dist/{chunk-Z5U6WE22.js.map → chunk-PRZPGRIK.js.map} +0 -0
  66. /package/dist/{chunk-KCPW4K7L.js.map → chunk-PZQUUBEB.js.map} +0 -0
  67. /package/dist/{chunk-5WJOPANS.js.map → chunk-Y5THZH5N.js.map} +0 -0
  68. /package/dist/{lazy-manifest-AJ6XPJ4F.js.map → lazy-manifest-BLZ3UZLX.js.map} +0 -0
  69. /package/dist/{localSources-SSIEH5F5.js.map → localSources-I6SIQ6CX.js.map} +0 -0
@@ -87,6 +87,7 @@ import {
87
87
  } from "./browser-control-server";
88
88
  import { parseExecResponseBanner } from "./exec-banner";
89
89
  import { controllerStreamRequest, parseControllerStreamResponse } from "./browser-control-stream";
90
+ import type { BrowserControlHttpCall, BrowserControlHttpOutcome } from "./browser-control-http";
90
91
  import {
91
92
  buildStreamUrl,
92
93
  exposedPortAllowsHostFetch,
@@ -177,6 +178,8 @@ export type BrowserControlPlacementSession = {
177
178
  ensureBrowserControl?: (
178
179
  request: BrowserControlEnsureRequest,
179
180
  ) => Promise<BrowserControlEnsureResponse>;
181
+ /** Agent-native controller transport, addressed by authority scope. */
182
+ browserControlHttp?: (request: BrowserControlHttpCall) => Promise<BrowserControlHttpOutcome>;
180
183
  openBrowserFrames?: (
181
184
  request: BrowserFramesOpenRequest,
182
185
  ) => Promise<{ channel: StreamChannel; endpoint: ExposedPortEndpoint }>;
@@ -267,6 +270,8 @@ export type PlacementBrowserSessionReference = {
267
270
 
268
271
  export type PlacementBrowserSession = PlacementBrowserSessionReference & {
269
272
  observation: BrowserObservationValue;
273
+ /** The tab inventory at creation, when the controller reports it. */
274
+ targets?: BrowserTargetValue[];
270
275
  };
271
276
 
272
277
  export type PlacementComputerSessionReference = {
@@ -412,6 +417,73 @@ export type PlacementBrowserStateCaptureReceipt = PlacementBrowserSessionReferen
412
417
  };
413
418
  };
414
419
 
420
+ /** The exact controller authority a preserved working copy was launched
421
+ * under; the controller refuses any other. */
422
+ export type PlacementBrowserWorkingCopyAuthority = PlacementBrowserSessionReference & {
423
+ tokenGeneration: number;
424
+ controlToken: string;
425
+ viewToken: string;
426
+ };
427
+
428
+ export const BROWSER_WORKING_COPY_REFUSALS = [
429
+ "unsupported_platform",
430
+ "browser_running",
431
+ "working_copy_invalid",
432
+ "receipt_not_adoptable",
433
+ "authority_changed",
434
+ "process_state_unknown",
435
+ "adoption_not_needed",
436
+ "adoption_unresolved",
437
+ ] as const;
438
+ export type PlacementBrowserWorkingCopyRefusal = (typeof BROWSER_WORKING_COPY_REFUSALS)[number];
439
+
440
+ export const BROWSER_WORKING_COPY_ADOPTION_PHASES = [
441
+ "intent",
442
+ "copied",
443
+ "receipt",
444
+ "source_preserved",
445
+ "adopted",
446
+ "interrupted",
447
+ "failed",
448
+ "collision",
449
+ "reverting",
450
+ "reverted",
451
+ ] as const;
452
+ export type PlacementBrowserWorkingCopyAdoptionPhase =
453
+ (typeof BROWSER_WORKING_COPY_ADOPTION_PHASES)[number];
454
+
455
+ export type PlacementBrowserWorkingCopyAdoption = PlacementBrowserSessionReference & {
456
+ operationId: string;
457
+ manifestDigest: string;
458
+ phase: PlacementBrowserWorkingCopyAdoptionPhase;
459
+ failureCode: string | null;
460
+ /** Directories this adoption kept beside the browser's own, by name. */
461
+ retainedDirectories: string[];
462
+ createdAt: string;
463
+ updatedAt: string;
464
+ };
465
+
466
+ export type PlacementBrowserWorkingCopyInspection = PlacementBrowserSessionReference & {
467
+ platformSupported: boolean;
468
+ refusal: PlacementBrowserWorkingCopyRefusal | null;
469
+ /** Exact digest of the bytes an adoption would take; null when refused. */
470
+ manifest: { digest: string; entries: number; bytes: number; excluded: string[] } | null;
471
+ profileIdentityMatches: boolean;
472
+ /** false only when no process provably uses the profile. */
473
+ profileInUse: boolean | null;
474
+ receipt: {
475
+ operationId: string;
476
+ intent: "launch" | "suspend" | "retire" | "adopt";
477
+ state: string;
478
+ directoryLaunchAllowed: boolean;
479
+ authorityMatches: boolean;
480
+ /** True when this working copy was first launched from a saved checkpoint. */
481
+ launchedFromCheckpoint: boolean;
482
+ adoption: { operationId: string; manifestDigest: string } | null;
483
+ } | null;
484
+ activeAdoption: PlacementBrowserWorkingCopyAdoption | null;
485
+ };
486
+
415
487
  export type ProvisionBrowserControlClientInput = {
416
488
  adminToken: string;
417
489
  adminTokenFile?: string;
@@ -649,6 +721,7 @@ export class BrowserControlClient {
649
721
  return {
650
722
  ...returnedReference,
651
723
  observation: BrowserObservation.parse(data.observation),
724
+ ...creationTargets(data.targets, returnedReference),
652
725
  };
653
726
  }
654
727
 
@@ -879,6 +952,103 @@ export class BrowserControlClient {
879
952
  return receipt;
880
953
  }
881
954
 
955
+ /** Read-only evidence about a preserved working copy whose controller
956
+ * memory was lost: the exact digest of its bytes and whether it can be
957
+ * adopted. Hashing a large profile can take minutes. */
958
+ async inspectWorkingCopy(
959
+ input: PlacementBrowserWorkingCopyAuthority,
960
+ ): Promise<PlacementBrowserWorkingCopyInspection> {
961
+ const reference = parseReference(input);
962
+ const data = await this.requestJson({
963
+ method: "POST",
964
+ path: `/v1/browser-sessions/${reference.browserSessionId}/working-copy/inspection`,
965
+ token: this.adminToken,
966
+ body: workingCopyAuthorityBody(reference, input),
967
+ timeoutMs: BROWSER_STATE_TRANSFER_TIMEOUT_MS,
968
+ });
969
+ return parseWorkingCopyInspection(data, reference);
970
+ }
971
+
972
+ /** Adopt the exact inspected bytes of a preserved working copy. A lost reply
973
+ * is checked with {@link workingCopyAdoption}. Sending the same operation id
974
+ * again never repeats a side effect: it only settles what that operation
975
+ * already did, under the authority that approved it. */
976
+ async adoptWorkingCopy(
977
+ input: PlacementBrowserWorkingCopyAuthority & { operationId: string; manifestDigest: string },
978
+ ): Promise<PlacementBrowserWorkingCopyAdoption> {
979
+ const reference = parseReference(input);
980
+ const operationId = requireUuid(input.operationId, "working copy adoption id");
981
+ const data = await this.requestJson({
982
+ method: "POST",
983
+ path: `/v1/browser-sessions/${reference.browserSessionId}/working-copy/adoptions`,
984
+ token: this.adminToken,
985
+ body: {
986
+ ...workingCopyAuthorityBody(reference, input),
987
+ operationId,
988
+ manifestDigest: sha256(input.manifestDigest, "working copy manifest digest"),
989
+ },
990
+ timeoutMs: BROWSER_STATE_TRANSFER_TIMEOUT_MS,
991
+ });
992
+ const adoption = parseWorkingCopyAdoption(data, reference, "current");
993
+ if (adoption.operationId !== operationId) {
994
+ throw new BrowserControlProtocolError("browser controller returned another adoption");
995
+ }
996
+ return adoption;
997
+ }
998
+
999
+ /** The recorded state of one adoption, read without changing it. */
1000
+ async workingCopyAdoption(
1001
+ reference: PlacementBrowserSessionReference,
1002
+ operationId: string,
1003
+ ): Promise<{ inFlight: boolean; adoption: PlacementBrowserWorkingCopyAdoption | null }> {
1004
+ const binding = parseReference(reference);
1005
+ const id = requireUuid(operationId, "working copy adoption id");
1006
+ const data = await this.requestJson({
1007
+ method: "GET",
1008
+ path: `/v1/browser-sessions/${binding.browserSessionId}/working-copy/adoptions/${id}`,
1009
+ token: this.adminToken,
1010
+ });
1011
+ if (!isRecord(data) || typeof data.inFlight !== "boolean") {
1012
+ throw new BrowserControlProtocolError(
1013
+ "browser controller returned malformed adoption status",
1014
+ );
1015
+ }
1016
+ const adoption =
1017
+ data.adoption === null ? null : parseWorkingCopyAdoption(data.adoption, binding, "recorded");
1018
+ if (adoption && adoption.operationId !== id) {
1019
+ throw new BrowserControlProtocolError("browser controller returned another adoption");
1020
+ }
1021
+ return { inFlight: data.inFlight, adoption };
1022
+ }
1023
+
1024
+ /** Revert one unfinished adoption under the browser's current authority:
1025
+ * its own directory goes back to the live path and the staged copy is
1026
+ * kept. Retrying the same revert finishes one that stopped part way. */
1027
+ async revertWorkingCopyAdoption(
1028
+ input: PlacementBrowserWorkingCopyAuthority & { operationId: string },
1029
+ ): Promise<{ inFlight: boolean; adoption: PlacementBrowserWorkingCopyAdoption | null }> {
1030
+ const reference = parseReference(input);
1031
+ const id = requireUuid(input.operationId, "working copy adoption id");
1032
+ const data = await this.requestJson({
1033
+ method: "POST",
1034
+ path: `/v1/browser-sessions/${reference.browserSessionId}/working-copy/adoptions/${id}/revert`,
1035
+ token: this.adminToken,
1036
+ body: workingCopyAuthorityBody(reference, input),
1037
+ timeoutMs: BROWSER_STATE_TRANSFER_TIMEOUT_MS,
1038
+ });
1039
+ if (!isRecord(data) || typeof data.inFlight !== "boolean") {
1040
+ throw new BrowserControlProtocolError("browser controller returned malformed revert status");
1041
+ }
1042
+ const adoption =
1043
+ data.adoption === null
1044
+ ? null
1045
+ : parseWorkingCopyAdoption(data.adoption, reference, "recorded");
1046
+ if (adoption && adoption.operationId !== id) {
1047
+ throw new BrowserControlProtocolError("browser controller returned another adoption");
1048
+ }
1049
+ return { inFlight: data.inFlight, adoption };
1050
+ }
1051
+
882
1052
  sessionClient(input: {
883
1053
  reference: PlacementBrowserSessionReference;
884
1054
  controlToken: string;
@@ -1176,6 +1346,19 @@ export class BrowserControlClient {
1176
1346
  );
1177
1347
  }
1178
1348
 
1349
+ const native = await this.requestNative(input, BROWSER_CONTROL_MAX_JSON_BYTES);
1350
+ if (native) {
1351
+ let text: string;
1352
+ try {
1353
+ text = new TextDecoder("utf-8", { fatal: true }).decode(native.body);
1354
+ } catch (error) {
1355
+ throw new BrowserControlProtocolError("browser controller response is not UTF-8", {
1356
+ cause: error,
1357
+ });
1358
+ }
1359
+ return parseEnvelope(text, native.status);
1360
+ }
1361
+
1179
1362
  if (
1180
1363
  this.session.execWithInput &&
1181
1364
  (input.body === undefined || Buffer.byteLength(JSON.stringify(input.body)) <= 32 * 1024)
@@ -1357,6 +1540,18 @@ export class BrowserControlClient {
1357
1540
  );
1358
1541
  }
1359
1542
 
1543
+ const nativeFrame = await this.requestNative(
1544
+ input,
1545
+ input.surface === "browser" ? 24 * 1024 * 1024 : COMPUTER_SCREENSHOT_MAX_BYTES,
1546
+ );
1547
+ if (nativeFrame) {
1548
+ if (nativeFrame.status < 200 || nativeFrame.status >= 300) {
1549
+ parseEnvelope(new TextDecoder().decode(nativeFrame.body), nativeFrame.status);
1550
+ throw new BrowserControlProtocolError("browser controller image response is invalid");
1551
+ }
1552
+ return controllerImageFrame(nativeFrame.body, nativeFrame.headers, input);
1553
+ }
1554
+
1360
1555
  if (this.session.execWithInput) {
1361
1556
  try {
1362
1557
  const response = await this.requestStream(
@@ -1512,8 +1707,69 @@ export class BrowserControlClient {
1512
1707
  }
1513
1708
 
1514
1709
  private async controllerPort(): Promise<number> {
1710
+ if (!this.nativeAuthority || !this.session.ensureBrowserControl) return this.port;
1711
+ return await this.ensureControllerPort(false);
1712
+ }
1713
+
1714
+ /** The agent-native transport: one in-process loopback request on the
1715
+ * Connected Machine, addressed by authority scope, with no process started.
1716
+ * Null when the placement does not serve it; the caller then keeps its
1717
+ * command transport. A controller that is not running receives nothing, so
1718
+ * it is ensured and the request sent once more whatever its method. */
1719
+ private async requestNative(
1720
+ input: { method: string; path: string; token: string; body?: unknown; timeoutMs?: number },
1721
+ maxResponseBytes: number,
1722
+ retry = true,
1723
+ ): Promise<{ status: number; headers: Map<string, string>; body: Uint8Array } | null> {
1724
+ const authority = this.nativeAuthority;
1725
+ if (!authority || !this.session.browserControlHttp || !this.session.ensureBrowserControl) {
1726
+ return null;
1727
+ }
1728
+ const method = input.method;
1729
+ if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "DELETE") {
1730
+ throw new BrowserControlProtocolError("browser controller request method is invalid");
1731
+ }
1732
+ // Validates the path exactly as the command transport does.
1733
+ localControllerUrl(this.port, input.path);
1734
+ const body =
1735
+ input.body === undefined ? undefined : new TextEncoder().encode(JSON.stringify(input.body));
1736
+ if (body !== undefined && body.byteLength > BROWSER_CONTROL_MAX_JSON_BYTES) {
1737
+ throw new RangeError("browser controller request body is too large");
1738
+ }
1739
+ let outcome: BrowserControlHttpOutcome;
1740
+ try {
1741
+ outcome = await this.session.browserControlHttp({
1742
+ scopeId: authority.scopeId,
1743
+ scopeGeneration: authority.scopeGeneration,
1744
+ method,
1745
+ path: input.path,
1746
+ bearerToken: requireToken(input.token, "browser controller token"),
1747
+ ...(body === undefined ? {} : { body }),
1748
+ timeoutMs: boundedTimeout(input.timeoutMs ?? this.timeoutMs),
1749
+ maxResponseBytes,
1750
+ });
1751
+ } catch (error) {
1752
+ if (retry && method === "GET") {
1753
+ await this.ensureControllerPort(true);
1754
+ return await this.requestNative(input, maxResponseBytes, false);
1755
+ }
1756
+ throw new BrowserControlTransportError("browser controller request transport failed", {
1757
+ cause: error,
1758
+ });
1759
+ }
1760
+ if (outcome.kind === "unsupported") return null;
1761
+ if (outcome.kind === "controller_absent") {
1762
+ if (!retry) throw new BrowserControlTransportError("browser controller is not running");
1763
+ await this.ensureControllerPort(true);
1764
+ return await this.requestNative(input, maxResponseBytes, false);
1765
+ }
1766
+ return outcome;
1767
+ }
1768
+
1769
+ private async ensureControllerPort(replace: boolean): Promise<number> {
1515
1770
  if (!this.nativeAuthority || !this.session.ensureBrowserControl) return this.port;
1516
1771
  const key = nativeControllerKey(this.nativeAuthority);
1772
+ if (replace) nativeControllerPorts.delete(key);
1517
1773
  const cached = nativeControllerPorts.get(key);
1518
1774
  if (cached !== undefined) return cached;
1519
1775
  const ensured = await this.session.ensureBrowserControl({
@@ -2615,6 +2871,28 @@ function parseReference(value: unknown): PlacementBrowserSessionReference {
2615
2871
  };
2616
2872
  }
2617
2873
 
2874
+ /** A create's tab inventory is a convenience: an older controller omits it,
2875
+ * and an inventory that does not validate, or that belongs to another
2876
+ * binding, is dropped rather than failing a browser that did launch. */
2877
+ function creationTargets(
2878
+ value: unknown,
2879
+ reference: PlacementBrowserSessionReference,
2880
+ ): { targets?: BrowserTargetValue[] } {
2881
+ if (!Array.isArray(value) || value.length > 10_000) return {};
2882
+ const targets: BrowserTargetValue[] = [];
2883
+ for (const entry of value) {
2884
+ const parsed = BrowserTarget.safeParse(entry);
2885
+ if (
2886
+ !parsed.success ||
2887
+ parsed.data.browserSessionId !== reference.browserSessionId ||
2888
+ parsed.data.controllerGeneration !== reference.controllerGeneration
2889
+ )
2890
+ return {};
2891
+ targets.push(parsed.data);
2892
+ }
2893
+ return { targets };
2894
+ }
2895
+
2618
2896
  function parseComputerReference(value: unknown): PlacementComputerSessionReference {
2619
2897
  if (!isRecord(value)) {
2620
2898
  throw new BrowserControlProtocolError("computer session binding is invalid");
@@ -2790,6 +3068,140 @@ function boundedProxyUrl(value: string): string {
2790
3068
  return url.toString();
2791
3069
  }
2792
3070
 
3071
+ function workingCopyAuthorityBody(
3072
+ reference: PlacementBrowserSessionReference,
3073
+ input: PlacementBrowserWorkingCopyAuthority,
3074
+ ) {
3075
+ return {
3076
+ controllerGeneration: reference.controllerGeneration,
3077
+ tokenGeneration: positiveSafeInteger(input.tokenGeneration, "token generation"),
3078
+ controlToken: requireToken(input.controlToken, "control token"),
3079
+ viewToken: requireToken(input.viewToken, "view token"),
3080
+ };
3081
+ }
3082
+
3083
+ /**
3084
+ * An adoption record. `generation: "current"` (a new adoption) requires the
3085
+ * record to belong to the requesting controller generation. `"recorded"`
3086
+ * (status, revert, inspection) keeps the generation the adoption was recorded
3087
+ * under: recovering an unfinished adoption after a controller restart means
3088
+ * reading and reverting a record made by an earlier generation, under the
3089
+ * browser's legitimate current control. The browser and operation always
3090
+ * have to match exactly.
3091
+ */
3092
+ function parseWorkingCopyAdoption(
3093
+ value: unknown,
3094
+ reference: PlacementBrowserSessionReference,
3095
+ generation: "current" | "recorded",
3096
+ ): PlacementBrowserWorkingCopyAdoption {
3097
+ if (
3098
+ !isRecord(value) ||
3099
+ value.browserSessionId !== reference.browserSessionId ||
3100
+ (generation === "current" && value.controllerGeneration !== reference.controllerGeneration) ||
3101
+ !BROWSER_WORKING_COPY_ADOPTION_PHASES.includes(
3102
+ value.phase as PlacementBrowserWorkingCopyAdoptionPhase,
3103
+ ) ||
3104
+ !(value.failureCode === null || typeof value.failureCode === "string") ||
3105
+ !Array.isArray(value.retainedDirectories) ||
3106
+ value.retainedDirectories.length > 64 ||
3107
+ !value.retainedDirectories.every(
3108
+ (name) => typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u.test(name),
3109
+ )
3110
+ ) {
3111
+ throw new BrowserControlProtocolError("browser controller returned a malformed adoption");
3112
+ }
3113
+ return {
3114
+ browserSessionId: reference.browserSessionId,
3115
+ controllerGeneration:
3116
+ generation === "current"
3117
+ ? reference.controllerGeneration
3118
+ : requireGeneration(value.controllerGeneration),
3119
+ operationId: requireUuid(value.operationId, "working copy adoption id"),
3120
+ manifestDigest: sha256(value.manifestDigest, "working copy manifest digest"),
3121
+ phase: value.phase as PlacementBrowserWorkingCopyAdoptionPhase,
3122
+ failureCode:
3123
+ value.failureCode === null ? null : requireOpaqueId(value.failureCode, "failure code"),
3124
+ retainedDirectories: value.retainedDirectories as string[],
3125
+ createdAt: timestamp(value.createdAt, "adoption created at"),
3126
+ updatedAt: timestamp(value.updatedAt, "adoption updated at"),
3127
+ };
3128
+ }
3129
+
3130
+ function parseWorkingCopyInspection(
3131
+ value: unknown,
3132
+ reference: PlacementBrowserSessionReference,
3133
+ ): PlacementBrowserWorkingCopyInspection {
3134
+ if (
3135
+ !isRecord(value) ||
3136
+ value.browserSessionId !== reference.browserSessionId ||
3137
+ value.controllerGeneration !== reference.controllerGeneration ||
3138
+ typeof value.platformSupported !== "boolean" ||
3139
+ typeof value.profileIdentityMatches !== "boolean" ||
3140
+ !(value.profileInUse === null || typeof value.profileInUse === "boolean") ||
3141
+ !(
3142
+ value.refusal === null ||
3143
+ BROWSER_WORKING_COPY_REFUSALS.includes(value.refusal as PlacementBrowserWorkingCopyRefusal)
3144
+ )
3145
+ ) {
3146
+ throw new BrowserControlProtocolError("browser controller returned a malformed inspection");
3147
+ }
3148
+ let manifest: PlacementBrowserWorkingCopyInspection["manifest"] = null;
3149
+ if (value.manifest !== null) {
3150
+ if (!isRecord(value.manifest)) {
3151
+ throw new BrowserControlProtocolError("browser controller returned a malformed manifest");
3152
+ }
3153
+ manifest = {
3154
+ digest: sha256(value.manifest.digest, "working copy manifest digest"),
3155
+ entries: nonnegativeSafeInteger(value.manifest.entries, "working copy entries"),
3156
+ bytes: nonnegativeSafeInteger(value.manifest.bytes, "working copy bytes"),
3157
+ excluded: excludedWorkingCopyEntries(value.manifest.excluded),
3158
+ };
3159
+ }
3160
+ let receipt: PlacementBrowserWorkingCopyInspection["receipt"] = null;
3161
+ if (value.receipt !== null) {
3162
+ const raw = value.receipt;
3163
+ if (
3164
+ !isRecord(raw) ||
3165
+ !["launch", "suspend", "retire", "adopt"].includes(raw.intent as string) ||
3166
+ typeof raw.state !== "string" ||
3167
+ typeof raw.directoryLaunchAllowed !== "boolean" ||
3168
+ typeof raw.authorityMatches !== "boolean" ||
3169
+ !(raw.restoreAuthorityDigest === null || typeof raw.restoreAuthorityDigest === "string") ||
3170
+ !(raw.adoption === null || isRecord(raw.adoption))
3171
+ ) {
3172
+ throw new BrowserControlProtocolError("browser controller returned a malformed receipt");
3173
+ }
3174
+ receipt = {
3175
+ operationId: requireUuid(raw.operationId, "working runtime receipt id"),
3176
+ intent: raw.intent as "launch" | "suspend" | "retire" | "adopt",
3177
+ state: requireOpaqueId(raw.state, "working runtime receipt state"),
3178
+ directoryLaunchAllowed: raw.directoryLaunchAllowed,
3179
+ authorityMatches: raw.authorityMatches,
3180
+ launchedFromCheckpoint: raw.restoreAuthorityDigest !== null,
3181
+ adoption: isRecord(raw.adoption)
3182
+ ? {
3183
+ operationId: requireUuid(raw.adoption.operationId, "working copy adoption id"),
3184
+ manifestDigest: sha256(raw.adoption.manifestDigest, "working copy manifest digest"),
3185
+ }
3186
+ : null,
3187
+ };
3188
+ }
3189
+ return {
3190
+ browserSessionId: reference.browserSessionId,
3191
+ controllerGeneration: reference.controllerGeneration,
3192
+ platformSupported: value.platformSupported,
3193
+ refusal: value.refusal as PlacementBrowserWorkingCopyRefusal | null,
3194
+ manifest,
3195
+ profileIdentityMatches: value.profileIdentityMatches,
3196
+ profileInUse: value.profileInUse,
3197
+ receipt,
3198
+ activeAdoption:
3199
+ value.activeAdoption === null || value.activeAdoption === undefined
3200
+ ? null
3201
+ : parseWorkingCopyAdoption(value.activeAdoption, reference, "recorded"),
3202
+ };
3203
+ }
3204
+
2793
3205
  function parseStateCaptureReceipt(value: unknown): PlacementBrowserStateCaptureReceipt {
2794
3206
  if (!isRecord(value) || !isRecord(value.manifest)) {
2795
3207
  throw new BrowserControlProtocolError(
@@ -3598,6 +4010,19 @@ function nonnegativeSafeInteger(value: unknown, label: string): number {
3598
4010
  return value;
3599
4011
  }
3600
4012
 
4013
+ function excludedWorkingCopyEntries(value: unknown): string[] {
4014
+ if (
4015
+ !Array.isArray(value) ||
4016
+ value.length > 16 ||
4017
+ !value.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 256)
4018
+ ) {
4019
+ throw new BrowserControlProtocolError(
4020
+ "browser controller returned malformed working copy exclusions",
4021
+ );
4022
+ }
4023
+ return [...value];
4024
+ }
4025
+
3601
4026
  function parseUnsignedInteger(value: string, label: string): number {
3602
4027
  const trimmed = value.trim();
3603
4028
  if (!/^(0|[1-9][0-9]*)$/u.test(trimmed)) {
@@ -0,0 +1,32 @@
1
+ /** One request to an agent-owned browser/computer controller, addressed by its
2
+ * exact authority scope rather than a local port. */
3
+ export type BrowserControlHttpCall = {
4
+ scopeId: string;
5
+ scopeGeneration: string;
6
+ method: "GET" | "POST" | "PUT" | "DELETE";
7
+ /** Absolute controller path with optional query. */
8
+ path: string;
9
+ bearerToken: string;
10
+ /** JSON request body bytes, when the request has a body. */
11
+ body?: Uint8Array;
12
+ timeoutMs: number;
13
+ maxResponseBytes: number;
14
+ };
15
+
16
+ /** What a placement transport did with one controller request. */
17
+ export type BrowserControlHttpOutcome =
18
+ /** The placement cannot serve it natively; nothing was sent. */
19
+ | { kind: "unsupported" }
20
+ /** No controller for that scope and generation accepted it; nothing was
21
+ * delivered, so the caller may ensure the controller and send again. */
22
+ | { kind: "controller_absent" }
23
+ | {
24
+ kind: "response";
25
+ status: number;
26
+ /** Lowercase header names. */
27
+ headers: Map<string, string>;
28
+ body: Uint8Array;
29
+ };
30
+
31
+ /** Extra time beyond the request's own deadline for the reply to arrive. */
32
+ export const BROWSER_CONTROL_HTTP_REPLY_GRACE_MS = 5_000;
@@ -361,6 +361,10 @@ export {
361
361
  type PlacementBrowserNetworkRoute,
362
362
  type PlacementBrowserTransport,
363
363
  type PlacementBrowserStateCaptureReceipt,
364
+ type PlacementBrowserWorkingCopyAdoption,
365
+ type PlacementBrowserWorkingCopyAuthority,
366
+ type PlacementBrowserWorkingCopyInspection,
367
+ type PlacementBrowserWorkingCopyRefusal,
364
368
  type ComputerControlFrame,
365
369
  type ComputerControlFrameMetadata,
366
370
  type ComputerFrameEvidenceMismatchReason,
@@ -67,6 +67,7 @@ export interface SelfhostedConnectionBinding {
67
67
  operationResourcePolicySupported: boolean;
68
68
  operationCpuQuotaSupported: boolean;
69
69
  transactionalFsWriteSupported?: boolean;
70
+ browserControlHttpSupported?: boolean;
70
71
  }
71
72
 
72
73
  export interface ActiveBackendResolverDeps {
@@ -406,6 +407,7 @@ export function makeActiveBackendResolver(
406
407
  operationResourcePolicySupported: connection.operationResourcePolicySupported,
407
408
  operationCpuQuotaSupported: connection.operationCpuQuotaSupported,
408
409
  transactionalFsWriteSupported: connection.transactionalFsWriteSupported === true,
410
+ browserControlHttpSupported: connection.browserControlHttpSupported === true,
409
411
  // The routing proxy caches this session by sandbox+epoch, but command
410
412
  // policy and runner authority are mutable. Re-read one coherent DB
411
413
  // snapshot only when a new exec/Git is admitted.