@opengeni/runtime 3.2.2 → 3.3.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/index.js CHANGED
@@ -187,7 +187,7 @@ import {
187
187
  workspaceEnvironmentInstructions,
188
188
  wrapAttemptToolDefinitions,
189
189
  wrapAttemptToolExecute
190
- } from "./chunk-2AUDDDO2.js";
190
+ } from "./chunk-XRX57ZQO.js";
191
191
  import {
192
192
  ActiveBackendUnresolvableError,
193
193
  BROWSER_CONTROL_ADMIN_TOKEN_FILE,
@@ -403,7 +403,7 @@ import {
403
403
  withRunCredentialsSession,
404
404
  withoutSandboxProviderIdentity,
405
405
  workspaceArchiveDownloadTemporaryDirectory
406
- } from "./chunk-462CH77G.js";
406
+ } from "./chunk-EK7IW2FR.js";
407
407
  import {
408
408
  SKILL_MAX_FILES,
409
409
  SKILL_MAX_FILE_BYTES,
@@ -3,6 +3,7 @@ import { type BrowserFrameMetadata } from "@opengeni/sdk";
3
3
  import { BROWSER_PROFILE_ARTIFACT_FORMAT, type BrowserActionCommand as BrowserActionCommandValue, type BrowserActionReceipt as BrowserActionReceiptValue, type BrowserClipboard as BrowserClipboardValue, type BrowserDiagnosticBatch as BrowserDiagnosticBatchValue, type BrowserDownload as BrowserDownloadValue, type BrowserDownloadExportReceipt as BrowserDownloadExportReceiptValue, type BrowserDownloadExportRequest as BrowserDownloadExportRequestValue, type BrowserDownloadListResponse as BrowserDownloadListResponseValue, type BrowserDomReadRequest as BrowserDomReadRequestValue, type BrowserDomReadResponse as BrowserDomReadResponseValue, type BrowserExternalAuthCommand as BrowserExternalAuthCommandValue, type BrowserExternalAuthResult as BrowserExternalAuthResultValue, type BrowserDiagnosticKind, type BrowserTargetListResponse as BrowserTargetListResponseValue, type BrowserObservation as BrowserObservationValue, type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue, type BrowserTarget as BrowserTargetValue, type BrowserTargetState as BrowserTargetStateValue, type BrowserWorkspaceFileStageRequest as BrowserWorkspaceFileStageRequestValue, type BrowserWorkspaceFileStageResponse as BrowserWorkspaceFileStageResponseValue, type BrowserPasskeySyncRequest as BrowserPasskeySyncRequestValue, type BrowserPasskeySyncResponse as BrowserPasskeySyncResponseValue, type ComputerActionCommand as ComputerActionCommandValue, type ComputerActionReceipt as ComputerActionReceiptValue, type ComputerClipboard as ComputerClipboardValue, type ComputerObservation as ComputerObservationValue, type ComputerSessionCapabilities as ComputerSessionCapabilitiesValue, type ComputerTarget as ComputerTargetValue, type InteractionError as InteractionErrorValue, type NetworkRouteConsistency as NetworkRouteConsistencyValue } from "@opengeni/contracts";
4
4
  import type { BrowserControlEnsureRequest, BrowserControlEnsureResponse, BrowserFramesOpenRequest, ComputerFramesOpenRequest, StreamChannel } from "@opengeni/agent-proto";
5
5
  import { type EnsureBrowserControlServerResult } from "./browser-control-server.js";
6
+ import type { BrowserControlHttpCall, BrowserControlHttpOutcome } from "./browser-control-http.js";
6
7
  import { type ExposedPortEndpoint } from "./stream-port.js";
7
8
  export declare const BROWSER_CONTROL_ADMIN_TOKEN_FILE = "/tmp/opengeni-browserd/authority/admin-token";
8
9
  type ExecResultLike = {
@@ -68,6 +69,8 @@ export type BrowserControlPlacementSession = {
68
69
  }) => void;
69
70
  };
70
71
  ensureBrowserControl?: (request: BrowserControlEnsureRequest) => Promise<BrowserControlEnsureResponse>;
72
+ /** Agent-native controller transport, addressed by authority scope. */
73
+ browserControlHttp?: (request: BrowserControlHttpCall) => Promise<BrowserControlHttpOutcome>;
71
74
  openBrowserFrames?: (request: BrowserFramesOpenRequest) => Promise<{
72
75
  channel: StreamChannel;
73
76
  endpoint: ExposedPortEndpoint;
@@ -434,6 +437,13 @@ export declare class BrowserControlClient {
434
437
  private requestBytes;
435
438
  private requestStream;
436
439
  private controllerPort;
440
+ /** The agent-native transport: one in-process loopback request on the
441
+ * Connected Machine, addressed by authority scope, with no process started.
442
+ * Null when the placement does not serve it; the caller then keeps its
443
+ * command transport. A controller that is not running receives nothing, so
444
+ * it is ensured and the request sent once more whatever its method. */
445
+ private requestNative;
446
+ private ensureControllerPort;
437
447
  }
438
448
  export declare class BrowserControlSessionClient {
439
449
  readonly reference: PlacementBrowserSessionReference;
@@ -0,0 +1,33 @@
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
+ /** What a placement transport did with one controller request. */
16
+ export type BrowserControlHttpOutcome =
17
+ /** The placement cannot serve it natively; nothing was sent. */
18
+ {
19
+ kind: "unsupported";
20
+ }
21
+ /** No controller for that scope and generation accepted it; nothing was
22
+ * delivered, so the caller may ensure the controller and send again. */
23
+ | {
24
+ kind: "controller_absent";
25
+ } | {
26
+ kind: "response";
27
+ status: number;
28
+ /** Lowercase header names. */
29
+ headers: Map<string, string>;
30
+ body: Uint8Array;
31
+ };
32
+ /** Extra time beyond the request's own deadline for the reply to arrive. */
33
+ export declare const BROWSER_CONTROL_HTTP_REPLY_GRACE_MS = 5000;
@@ -213,7 +213,7 @@ import {
213
213
  withRunCredentialsSession,
214
214
  withoutSandboxProviderIdentity,
215
215
  workspaceArchiveDownloadTemporaryDirectory
216
- } from "../chunk-462CH77G.js";
216
+ } from "../chunk-EK7IW2FR.js";
217
217
  import "../chunk-5ULGMFKR.js";
218
218
  import {
219
219
  CODE_SEARCH_RG_MAX_BYTES,
@@ -32,6 +32,7 @@ export interface SelfhostedConnectionBinding {
32
32
  operationResourcePolicySupported: boolean;
33
33
  operationCpuQuotaSupported: boolean;
34
34
  transactionalFsWriteSupported?: boolean;
35
+ browserControlHttpSupported?: boolean;
35
36
  }
36
37
  export interface ActiveBackendResolverDeps {
37
38
  /** The workspace the session belongs to (the control-plane subject scope). */
@@ -1,6 +1,7 @@
1
1
  import { ControlResponse, type BrowserControlEnsureRequest, type BrowserControlEnsureResponse, type BrowserFramesOpenRequest, type ComputerFramesOpenRequest, type StreamChannel } from "@opengeni/agent-proto";
2
2
  import { Manifest } from "@openai/agents/sandbox";
3
3
  import type { ExposedPortEndpoint } from "../stream-port.js";
4
+ import { type BrowserControlHttpCall, type BrowserControlHttpOutcome } from "../browser-control-http.js";
4
5
  import { type ControlRpc } from "./control-rpc.js";
5
6
  import { type SelfhostedRetryClock } from "./retry-policy.js";
6
7
  import type { SelfhostedOpObserver } from "./op-observer.js";
@@ -100,6 +101,8 @@ export interface SelfhostedBackgroundCommandAdoption {
100
101
  * every exec/Git admission; retries and already-started operations retain it. */
101
102
  export interface SelfhostedOperationAdmission {
102
103
  transactionalFsWriteSupported?: boolean;
104
+ /** The live runner serves in-process browser/computer controller requests. */
105
+ browserControlHttpSupported?: boolean;
103
106
  /** Workspace segment of the physical agent/relay route. Personal machines may
104
107
  * originate in a different same-organization workspace than the session. */
105
108
  workspaceId?: string;
@@ -123,6 +126,9 @@ export interface SelfhostedOperationAdmission {
123
126
  export interface SelfhostedSessionDeps {
124
127
  /** Exact live daemon capability; absent preserves legacy small-file writes. */
125
128
  transactionalFsWriteSupported?: boolean;
129
+ /** Exact live daemon capability; absent keeps controller requests on the
130
+ * command transport. */
131
+ browserControlHttpSupported?: boolean;
126
132
  /** Authorization/session workspace. */
127
133
  workspaceId: string;
128
134
  /** Physical control-plane workspace. Defaults to workspaceId for legacy and
@@ -303,6 +309,7 @@ export declare class SelfhostedSession {
303
309
  private readonly resourcePolicySupported;
304
310
  private readonly operationCpuQuotaSupported;
305
311
  private readonly transactionalFsWriteSupported;
312
+ private readonly browserControlHttpSupported;
306
313
  private readonly defaultOpStream;
307
314
  private readonly connectionInstanceId;
308
315
  private readonly resolveOperationAdmission;
@@ -524,6 +531,14 @@ export declare class SelfhostedSession {
524
531
  /** Ensure the loopback browser controller sidecar owned by this connected
525
532
  * machine. The authority scope is supplied by the caller and remains local. */
526
533
  ensureBrowserControl(request: BrowserControlEnsureRequest): Promise<BrowserControlEnsureResponse>;
534
+ /** Send one request to the agent-owned controller of an exact authority
535
+ * scope, served inside the runner over loopback without starting a process.
536
+ * `unsupported` means this connection's runner does not serve it and nothing
537
+ * was sent. `controller_absent` means no controller for that scope and
538
+ * generation accepted the request, so it was not delivered and the caller may
539
+ * ensure the controller and send again. A body too large for one reply is
540
+ * drained from the runner's spool; the request is never repeated. */
541
+ browserControlHttp(request: BrowserControlHttpCall): Promise<BrowserControlHttpOutcome>;
527
542
  /** Open one browser-native frame producer and return its canonical relay
528
543
  * endpoint. This does not route through `resolveExposedPort`: that legacy
529
544
  * method allocates PTY/desktop resources based on fixed ports, while this op
@@ -597,6 +612,7 @@ export declare class SelfhostedSandboxClient {
597
612
  private readonly operationResourcePolicySupported;
598
613
  private readonly operationCpuQuotaSupported;
599
614
  private readonly transactionalFsWriteSupported;
615
+ private readonly browserControlHttpSupported;
600
616
  private readonly resolveOperationAdmission;
601
617
  private readonly adoptBackgroundCommand;
602
618
  private readonly settleBackgroundCommand;
@@ -631,6 +647,7 @@ export declare class SelfhostedSandboxClient {
631
647
  operationResourcePolicySupported?: boolean;
632
648
  operationCpuQuotaSupported?: boolean;
633
649
  transactionalFsWriteSupported?: boolean;
650
+ browserControlHttpSupported?: boolean;
634
651
  resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
635
652
  adoptBackgroundCommand?: SelfhostedSessionDeps["adoptBackgroundCommand"];
636
653
  settleBackgroundCommand?: SelfhostedSessionDeps["settleBackgroundCommand"];
@@ -711,6 +728,8 @@ export interface SelfhostedSessionBuild {
711
728
  /** Exact live CPU enforcement capability paired with the initial snapshot. */
712
729
  operationCpuQuotaSupported?: boolean;
713
730
  transactionalFsWriteSupported?: boolean;
731
+ /** Exact live runner capability for in-process controller requests. */
732
+ browserControlHttpSupported?: boolean;
714
733
  /** Live last-boundary operation admission resolver; see SelfhostedSessionDeps. */
715
734
  resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
716
735
  adoptBackgroundCommand?: SelfhostedSessionDeps["adoptBackgroundCommand"];
@@ -3,8 +3,8 @@ import {
3
3
  prepareAgentTools,
4
4
  secretArgumentMode,
5
5
  sessionMcpApprovalConnectionId
6
- } from "./chunk-2AUDDDO2.js";
7
- import "./chunk-462CH77G.js";
6
+ } from "./chunk-XRX57ZQO.js";
7
+ import "./chunk-EK7IW2FR.js";
8
8
  import "./chunk-4KFT2G3W.js";
9
9
  import "./chunk-7OUG7WBV.js";
10
10
  import "./chunk-5ULGMFKR.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/runtime",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -75,19 +75,19 @@
75
75
  "@openai/agents": "0.14.3",
76
76
  "@openai/agents-core": "0.14.3",
77
77
  "@openai/agents-extensions": "0.14.3",
78
- "@opengeni/agent-proto": "3.2.2",
79
- "@opengeni/capabilities": "3.2.2",
80
- "@opengeni/codemode": "3.2.2",
81
- "@opengeni/codex": "3.2.2",
82
- "@opengeni/config": "3.2.2",
83
- "@opengeni/contracts": "3.2.2",
84
- "@opengeni/interaction": "3.2.2",
85
- "@opengeni/network": "3.2.2",
86
- "@opengeni/observability": "3.2.2",
87
- "@opengeni/sdk": "3.2.2",
88
- "@opengeni/tool-gateway": "3.2.2",
89
- "@opengeni/vaults": "3.2.2",
90
- "@opengeni/xai-subscription": "3.2.2",
78
+ "@opengeni/agent-proto": "3.3.0",
79
+ "@opengeni/capabilities": "3.3.0",
80
+ "@opengeni/codemode": "3.3.0",
81
+ "@opengeni/codex": "3.3.0",
82
+ "@opengeni/config": "3.3.0",
83
+ "@opengeni/contracts": "3.3.0",
84
+ "@opengeni/interaction": "3.3.0",
85
+ "@opengeni/network": "3.3.0",
86
+ "@opengeni/observability": "3.3.0",
87
+ "@opengeni/sdk": "3.3.0",
88
+ "@opengeni/tool-gateway": "3.3.0",
89
+ "@opengeni/vaults": "3.3.0",
90
+ "@opengeni/xai-subscription": "3.3.0",
91
91
  "debug": "4.4.3",
92
92
  "modal": "0.9.0",
93
93
  "openai": "6.47.0",
@@ -18,8 +18,9 @@ Site on the web is built in (see "Publish to the web").
18
18
 
19
19
  A Site never needs its own server, API key, or hosting provider to reach
20
20
  Opengeni. Model access, agent conversations, and workspace tools all come from
21
- the host bridge: `createOpenGeniSiteClient` from `@opengeni/sdk/site` plus the
22
- `@opengeni/react/session-ui` conversation surface. The parent page owns the
21
+ the host bridge: `createOpenGeniSiteClient` from `@opengeni/sdk/site`. Use its
22
+ `ai.run` helper for inline results, or `@opengeni/react/session-ui` when the app
23
+ needs a conversation surface. The parent page owns the
23
24
  credentials and workspace identity; Site code receives neither, so "keep
24
25
  credentials server-side" is already satisfied by that bridge.
25
26
 
@@ -239,6 +240,33 @@ const client = createOpenGeniSiteClient();
239
240
  const issues = await client.tools.linear.issues_list({ state: "Todo" });
240
241
  ```
241
242
 
243
+ ### Inline AI: the app chooses the interaction
244
+
245
+ Use `site.ai.run({ initialMessage: appDefinedPrompt, idempotencyKey: actionId })`
246
+ to return AI output inside the app without mounting chat or displaying history.
247
+ Prompts, context, controls and results are app-designed; Draft, Summarize or
248
+ Classify are merely possible examples, never required buttons or templates.
249
+ Use `onText` for incremental text and `onSession` to retain a native handle before
250
+ waiting. A result has `text`, `sessionId`, `turnId`, `after` and `status`:
251
+ `completed`, `pending` (approval/input needed), or `cancelled`; failures reject.
252
+ Validate any app-defined structured output before using it.
253
+
254
+ This is ordinary native execution with the same model access, subscriptions,
255
+ permissions, tool selection, approvals, metering and outsider caps. Aborting the
256
+ observation does not cancel the task. `site.ai.result(id, { turnId })` replays
257
+ without creating more work. Supplying `after` returns subsequent text only;
258
+ retain earlier text or omit it for a full replay. Keep retry keys stable per
259
+ action and handles in viewer-private state/storage, not shared Site Data.
260
+
261
+ Site AI is private per viewer by default and does not clutter the workspace
262
+ sidebar or ordinary search. Signed-in members use their real human identity;
263
+ guests use existing cookie/visitor continuity, not a shared anonymous chat.
264
+ If organization policy disallows private execution, surface its explicit
265
+ refusal; never fall back to shared sessions. Privacy does not automatically
266
+ extend to separately published artifacts or shared Site Data.
267
+
268
+ ### Optional conversation UI
269
+
242
270
  For embedded conversations, `site.client` is the ordinary Opengeni SDK client;
243
271
  `site.workspaceId` is a host-resolved routing alias. Use the normal React
244
272
  complete conversation surface—do not implement session REST or SSE yourself:
@@ -259,7 +287,7 @@ const site = createOpenGeniSiteClient();
259
287
  ```
260
288
 
261
289
  `sessionId` is the existing session or the id returned by `site.client.createSession`.
262
- For a normal chat app, offer a conversation selector and “New conversation”.
290
+ For a normal chat app, a conversation selector and “New conversation” are optional.
263
291
  Create on first Send rather than mounting the page; keep the selected id in React
264
292
  state and reopen existing conversations through the SDK.
265
293
  Keep first-send creation single-flight; reuse its `idempotencyKey` when retrying,
@@ -272,14 +300,17 @@ write origin metadata yourself. The local preview has no published identity, so
272
300
  this filter returns no Site history there; test newly created/current conversations
273
301
  locally and the selector after publication.
274
302
 
275
- This is a default, not a restriction. For a project-focused app, use `channelId`
276
- on SDK creation/listing instead; its list may include conversations created
277
- elsewhere. Project management tools use `projectId` (see `opengeni-projects`
278
- when available). Do not automatically create a project just to group Site chats.
279
- Unfiled Site conversations group under their Site in the main sidebar; explicit
280
- projects and pins take precedence. Origin remains visible regardless of placement.
281
- The host Site page always provides its own Conversations panel independently of
282
- the navigation you build. Opening existing sessions never changes their origin.
303
+ Do not automatically create a project just to group Site chats. Site execution
304
+ does not appear in ordinary workspace discovery, even for its owner. The host
305
+ Site page provides a viewer-scoped Conversations panel independently of app
306
+ navigation. Opening existing sessions never changes their origin or audience.
307
+ Page code cannot widen the Site boundary by supplying project, visibility,
308
+ tools or credential fields; use ordinary authoring tools for project management.
309
+
310
+ Per-Site AI costs are in Insights: actual credit charges are distinct from
311
+ API-equivalent subscription/own-key estimates and unbilled platform counters.
312
+ Usage is for limits. Do not build private transcript or viewer analytics into
313
+ shared dashboards just because aggregate costs are visible there.
283
314
 
284
315
  `SessionConversation` connects timeline/history, durable composer drafts,
285
316
  queue display/edit/delete/steer, pause/resume, and human-input forms. Prefer it
@@ -667,8 +698,11 @@ workspace first.
667
698
  `rate_limited`, `site_budget_exhausted` and retryable `site_budget_busy` (429), and
668
699
  `site_platform_brake_engaged` (503) as normal UI states.
669
700
  - The same Site HTML and Site Data work there; no rebuild is needed.
670
- - Every published-host agent gets this Site's `site_data_query` and `site_data_write`,
701
+ - Published-host agents get this Site's `site_data_query` and `site_data_write`,
671
702
  plus `site_sql_run` when SQL is enabled; members also get read-only `site_sql_query`.
703
+ Member tools are intersected with live permissions: a member without artifact
704
+ publishing can still use AI without write tools, and without data-read permission
705
+ can use plain AI without data tools. Never request extra permission just to chat.
672
706
  The server pins the Site and binds calls to the conversation's viewer, including
673
707
  collection ownership and query audiences. No management tools or sandbox are
674
708
  supplied; publication settings have no sandbox opt-in. Members get no Site
@@ -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 }>;
@@ -1176,6 +1179,19 @@ export class BrowserControlClient {
1176
1179
  );
1177
1180
  }
1178
1181
 
1182
+ const native = await this.requestNative(input, BROWSER_CONTROL_MAX_JSON_BYTES);
1183
+ if (native) {
1184
+ let text: string;
1185
+ try {
1186
+ text = new TextDecoder("utf-8", { fatal: true }).decode(native.body);
1187
+ } catch (error) {
1188
+ throw new BrowserControlProtocolError("browser controller response is not UTF-8", {
1189
+ cause: error,
1190
+ });
1191
+ }
1192
+ return parseEnvelope(text, native.status);
1193
+ }
1194
+
1179
1195
  if (
1180
1196
  this.session.execWithInput &&
1181
1197
  (input.body === undefined || Buffer.byteLength(JSON.stringify(input.body)) <= 32 * 1024)
@@ -1357,6 +1373,18 @@ export class BrowserControlClient {
1357
1373
  );
1358
1374
  }
1359
1375
 
1376
+ const nativeFrame = await this.requestNative(
1377
+ input,
1378
+ input.surface === "browser" ? 24 * 1024 * 1024 : COMPUTER_SCREENSHOT_MAX_BYTES,
1379
+ );
1380
+ if (nativeFrame) {
1381
+ if (nativeFrame.status < 200 || nativeFrame.status >= 300) {
1382
+ parseEnvelope(new TextDecoder().decode(nativeFrame.body), nativeFrame.status);
1383
+ throw new BrowserControlProtocolError("browser controller image response is invalid");
1384
+ }
1385
+ return controllerImageFrame(nativeFrame.body, nativeFrame.headers, input);
1386
+ }
1387
+
1360
1388
  if (this.session.execWithInput) {
1361
1389
  try {
1362
1390
  const response = await this.requestStream(
@@ -1512,8 +1540,69 @@ export class BrowserControlClient {
1512
1540
  }
1513
1541
 
1514
1542
  private async controllerPort(): Promise<number> {
1543
+ if (!this.nativeAuthority || !this.session.ensureBrowserControl) return this.port;
1544
+ return await this.ensureControllerPort(false);
1545
+ }
1546
+
1547
+ /** The agent-native transport: one in-process loopback request on the
1548
+ * Connected Machine, addressed by authority scope, with no process started.
1549
+ * Null when the placement does not serve it; the caller then keeps its
1550
+ * command transport. A controller that is not running receives nothing, so
1551
+ * it is ensured and the request sent once more whatever its method. */
1552
+ private async requestNative(
1553
+ input: { method: string; path: string; token: string; body?: unknown; timeoutMs?: number },
1554
+ maxResponseBytes: number,
1555
+ retry = true,
1556
+ ): Promise<{ status: number; headers: Map<string, string>; body: Uint8Array } | null> {
1557
+ const authority = this.nativeAuthority;
1558
+ if (!authority || !this.session.browserControlHttp || !this.session.ensureBrowserControl) {
1559
+ return null;
1560
+ }
1561
+ const method = input.method;
1562
+ if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "DELETE") {
1563
+ throw new BrowserControlProtocolError("browser controller request method is invalid");
1564
+ }
1565
+ // Validates the path exactly as the command transport does.
1566
+ localControllerUrl(this.port, input.path);
1567
+ const body =
1568
+ input.body === undefined ? undefined : new TextEncoder().encode(JSON.stringify(input.body));
1569
+ if (body !== undefined && body.byteLength > BROWSER_CONTROL_MAX_JSON_BYTES) {
1570
+ throw new RangeError("browser controller request body is too large");
1571
+ }
1572
+ let outcome: BrowserControlHttpOutcome;
1573
+ try {
1574
+ outcome = await this.session.browserControlHttp({
1575
+ scopeId: authority.scopeId,
1576
+ scopeGeneration: authority.scopeGeneration,
1577
+ method,
1578
+ path: input.path,
1579
+ bearerToken: requireToken(input.token, "browser controller token"),
1580
+ ...(body === undefined ? {} : { body }),
1581
+ timeoutMs: boundedTimeout(input.timeoutMs ?? this.timeoutMs),
1582
+ maxResponseBytes,
1583
+ });
1584
+ } catch (error) {
1585
+ if (retry && method === "GET") {
1586
+ await this.ensureControllerPort(true);
1587
+ return await this.requestNative(input, maxResponseBytes, false);
1588
+ }
1589
+ throw new BrowserControlTransportError("browser controller request transport failed", {
1590
+ cause: error,
1591
+ });
1592
+ }
1593
+ if (outcome.kind === "unsupported") return null;
1594
+ if (outcome.kind === "controller_absent") {
1595
+ if (!retry) throw new BrowserControlTransportError("browser controller is not running");
1596
+ await this.ensureControllerPort(true);
1597
+ return await this.requestNative(input, maxResponseBytes, false);
1598
+ }
1599
+ return outcome;
1600
+ }
1601
+
1602
+ private async ensureControllerPort(replace: boolean): Promise<number> {
1515
1603
  if (!this.nativeAuthority || !this.session.ensureBrowserControl) return this.port;
1516
1604
  const key = nativeControllerKey(this.nativeAuthority);
1605
+ if (replace) nativeControllerPorts.delete(key);
1517
1606
  const cached = nativeControllerPorts.get(key);
1518
1607
  if (cached !== undefined) return cached;
1519
1608
  const ensured = await this.session.ensureBrowserControl({
@@ -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;
@@ -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.