@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.
- package/dist/agent-LJT5NPWT.js +9 -0
- package/dist/assets/codemode-client.json +1 -1
- package/dist/browser-current-tab.d.ts +46 -0
- package/dist/browser-model-text.d.ts +126 -0
- package/dist/{chunk-2AUDDDO2.js → chunk-3DFH3ZHU.js} +784 -121
- package/dist/chunk-3DFH3ZHU.js.map +1 -0
- package/dist/{chunk-HJSIPTFA.js → chunk-4XDJ4TJA.js} +2 -2
- package/dist/{chunk-XQ23VCZ4.js → chunk-6FQ6KEII.js} +3 -3
- package/dist/{chunk-7LJ7KDPI.js → chunk-ERD4ICVB.js} +4 -3
- package/dist/{chunk-462CH77G.js → chunk-ETTCGPBT.js} +400 -9
- package/dist/chunk-ETTCGPBT.js.map +1 -0
- package/dist/{chunk-7OUG7WBV.js → chunk-GWRBALGQ.js} +181 -13
- package/dist/chunk-GWRBALGQ.js.map +1 -0
- package/dist/{chunk-YG26XE7B.js → chunk-JYDG2A7H.js} +2 -2
- package/dist/{chunk-RSHQSLEJ.js → chunk-KIL4FGX5.js} +3 -3
- package/dist/{chunk-NWS4HPB7.js → chunk-MIAL5I4O.js} +2 -2
- package/dist/{chunk-Z5U6WE22.js → chunk-PRZPGRIK.js} +2 -2
- package/dist/{chunk-KCPW4K7L.js → chunk-PZQUUBEB.js} +2 -2
- package/dist/{chunk-5WJOPANS.js → chunk-Y5THZH5N.js} +7 -7
- package/dist/codex-tool-search.d.ts +9 -0
- package/dist/gmail-rest-mcp.js +8 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -12
- package/dist/interaction-tool-json-schema.d.ts +27 -0
- package/dist/{lazy-manifest-AJ6XPJ4F.js → lazy-manifest-BLZ3UZLX.js} +4 -4
- package/dist/lazy-tool-transport.d.ts +10 -0
- package/dist/{localSources-SSIEH5F5.js → localSources-I6SIQ6CX.js} +6 -6
- package/dist/model-call.js +3 -3
- package/dist/sandbox/browser-control-client.d.ts +87 -0
- package/dist/sandbox/browser-control-http.d.ts +33 -0
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +7 -7
- package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
- package/dist/sandbox/selfhosted/session.d.ts +22 -0
- package/dist/slack-rest-mcp.js +8 -8
- package/dist/workspace-tool-gateway.js +12 -12
- package/package.json +14 -14
- package/src/browser-current-tab.ts +139 -0
- package/src/browser-model-text.ts +378 -0
- package/src/bundled_site_skills/opengeni-sites/SKILL.md +46 -12
- package/src/codex-tool-search.ts +53 -0
- package/src/index.ts +39 -1
- package/src/interaction-tool-json-schema.ts +281 -0
- package/src/interaction-tools.ts +425 -235
- package/src/lazy-tool-transport.ts +229 -10
- package/src/model-input.ts +12 -1
- package/src/sandbox/browser-control-client.ts +425 -0
- package/src/sandbox/browser-control-http.ts +32 -0
- package/src/sandbox/index.ts +4 -0
- package/src/sandbox/routing/backend-resolver.ts +2 -0
- package/src/sandbox/selfhosted/session.ts +154 -0
- package/dist/agent-CTPLA3HP.js +0 -9
- package/dist/browser-action-json-schema.d.ts +0 -4
- package/dist/chunk-2AUDDDO2.js.map +0 -1
- package/dist/chunk-462CH77G.js.map +0 -1
- package/dist/chunk-7OUG7WBV.js.map +0 -1
- package/src/browser-action-json-schema.ts +0 -10
- /package/dist/{agent-CTPLA3HP.js.map → agent-LJT5NPWT.js.map} +0 -0
- /package/dist/{chunk-HJSIPTFA.js.map → chunk-4XDJ4TJA.js.map} +0 -0
- /package/dist/{chunk-XQ23VCZ4.js.map → chunk-6FQ6KEII.js.map} +0 -0
- /package/dist/{chunk-7LJ7KDPI.js.map → chunk-ERD4ICVB.js.map} +0 -0
- /package/dist/{chunk-YG26XE7B.js.map → chunk-JYDG2A7H.js.map} +0 -0
- /package/dist/{chunk-RSHQSLEJ.js.map → chunk-KIL4FGX5.js.map} +0 -0
- /package/dist/{chunk-NWS4HPB7.js.map → chunk-MIAL5I4O.js.map} +0 -0
- /package/dist/{chunk-Z5U6WE22.js.map → chunk-PRZPGRIK.js.map} +0 -0
- /package/dist/{chunk-KCPW4K7L.js.map → chunk-PZQUUBEB.js.map} +0 -0
- /package/dist/{chunk-5WJOPANS.js.map → chunk-Y5THZH5N.js.map} +0 -0
- /package/dist/{lazy-manifest-AJ6XPJ4F.js.map → lazy-manifest-BLZ3UZLX.js.map} +0 -0
- /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;
|
package/src/sandbox/index.ts
CHANGED
|
@@ -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.
|