@opengeni/runtime 3.1.3-canary.38890816741001 → 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-GPCVTVNT.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-GPCVTVNT.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.1.3-canary.38890816741001",
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.1.3-canary.38890816741001",
79
- "@opengeni/capabilities": "3.1.3-canary.38890816741001",
80
- "@opengeni/codemode": "3.1.3-canary.38890816741001",
81
- "@opengeni/codex": "3.1.3-canary.38890816741001",
82
- "@opengeni/config": "3.1.3-canary.38890816741001",
83
- "@opengeni/contracts": "3.1.3-canary.38890816741001",
84
- "@opengeni/interaction": "3.1.3-canary.38890816741001",
85
- "@opengeni/network": "3.1.3-canary.38890816741001",
86
- "@opengeni/observability": "3.1.3-canary.38890816741001",
87
- "@opengeni/sdk": "3.1.3-canary.38890816741001",
88
- "@opengeni/tool-gateway": "3.1.3-canary.38890816741001",
89
- "@opengeni/vaults": "3.1.3-canary.38890816741001",
90
- "@opengeni/xai-subscription": "3.1.3-canary.38890816741001",
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",
@@ -193,3 +193,54 @@ README's Workspace tools section for approval and migration details.
193
193
  Never put tokens in prompts, Skills, `modelContext`, tool URLs, browser code,
194
194
  or logs. Opengeni stores Connection and MCP header secrets encrypted and keeps
195
195
  them out of the model's context.
196
+
197
+ ### Your product as a vault provider
198
+
199
+ Use native vaults when the agent needs to discover and use stored credentials.
200
+ An embedding product can expose multiple remote vaults through a signed
201
+ `external` connection; an in-process host can keep using
202
+ `connectionCredentials.vaultProvider`. Keep the simple chat/tool integration
203
+ above unchanged unless the product actually needs this optional capability.
204
+
205
+ Configure remote connections on the server with `vaultsApi(client, workspaceId)`
206
+ from `@opengeni/sdk`: `installVaultProvider`, `getVaultProviderCatalog`,
207
+ `listProviderVaults`, and `linkProviderVault`. Never put provider tokens or the
208
+ signing secret in browser code, model context, or URL parameters. Verify each
209
+ request's raw-body signature and map its native account/workspace/subject to
210
+ the host's actual tenant/user before authorizing it. A shared connection must
211
+ not return a different user's inventory into the same metadata mirror; use
212
+ separate user-owned connections for user-specific stores.
213
+
214
+ Connection setup uses the existing `connections:write` and `secrets:write`
215
+ permissions and native owner checks, including authorized agent attempts for
216
+ shared connections. Personal setup requires the verified owner, and
217
+ organization setup requires its admin. List/read/write operations always use
218
+ the registered connection endpoint, not a per-call URL chosen by a tool caller.
219
+
220
+ `getVaultProviderCatalog` includes deployment availability even before anyone
221
+ connects; disable unavailable setup choices instead of collecting credentials
222
+ that this deployment cannot use. `listVaultProviderConnections` remains a
223
+ metadata-only convenience when only existing connections are needed.
224
+
225
+ For a remote host, use `createExternalVaultReceiver` from the server-only
226
+ `@opengeni/sdk/external-vault-receiver` subpath and a durable atomic replay and
227
+ mutation store. The in-memory store is only for local tests. Its
228
+ `resolveConnection` binds the native connection to one fixed host inventory;
229
+ `authorize` checks the mapped person on every operation. If the host must
230
+ register the native connection ID first, install returns saved/pending setup;
231
+ register that ID, then verify. Never accept all unknown connections into one
232
+ default tenant to avoid this registration step.
233
+
234
+ Provider capabilities are granular: do not assume every read-write vault can
235
+ update TOTP, upload files, restore history or archive items. Distinguish cached
236
+ metadata from live value reads. Incomplete discovery or an unavailable source
237
+ is not proof that the user has no matching secret. For a saved-but-pending
238
+ setup (`discoveryPending: true`), use `verifyVaultProviderConnection` with the
239
+ connection ID rather than resubmitting a consumed bootstrap token.
240
+
241
+ The native secret ask card still saves to hosted vaults only. If the host must
242
+ own collection, collect in the host UI and expose the saved item via its
243
+ provider. Keep short-lived per-attempt credentials on the existing renewable
244
+ credential callback: a stored item does not replace expiry/renewal, Git/MCP
245
+ transport bindings or cleanup. See the product's external-vault and credential
246
+ provider documentation before implementing either path.
@@ -5,10 +5,8 @@ description: Use when signing in, creating accounts, storing or using passwords,
5
5
 
6
6
  # Secrets: sign in and use credentials yourself
7
7
 
8
- You are trusted with the user's vaults. Act on your own: sign in, create
9
- accounts when the task allows it, enroll your own 2FA, fetch email and SMS
10
- codes, and solve CAPTCHAs. You never need to see a value: write a reference and
11
- it resolves where it runs.
8
+ Use references instead of viewing values. Sign in, create accounts when the
9
+ task allows it, enroll 2FA, fetch codes and solve CAPTCHAs yourself.
12
10
 
13
11
  ## References
14
12
 
@@ -30,16 +28,15 @@ The `secret_*` tools take references as names and never resolve them.
30
28
 
31
29
  ## Tools
32
30
 
33
- - `secret_search {query?, website?, type?, vault?, cursor?}`: items and
34
- references, never values; no filters lists all, page one lists vaults.
35
- Repeat with `cursor` while `nextCursor` is set.
36
- - `secret_save {item, vault?, saveTo?, type?, websites?, username?, fields?, mode?}`:
37
- each field takes one of `value`, `generate`, `fromReference` or
38
- `restoreVersion`. Use `generate` for new passwords.
39
- - `secret_move {reference, toVault, mode, newName?}`: move or copy an item.
40
- - `secret_delete {reference}`: archive an item.
41
- - `secret_get {reference}`: plaintext, only with `secrets:read`. Use it only
42
- when a tool cannot take a reference.
31
+ - `secret_search`: metadata and references, never values. No filters lists all;
32
+ page one lists vaults. Follow `nextCursor` with unchanged filters. Check
33
+ `complete` and freshness before inferring absence. `refresh: true` requests
34
+ fresh inventory without bypassing rate limits.
35
+ - `secret_save`: each field takes `value`, `generate`, `fromReference` or
36
+ `restoreVersion`. Generate new passwords.
37
+ - `secret_move`: move or copy to another vault.
38
+ - `secret_delete`: archive an item.
39
+ - `secret_get`: plaintext with `secrets:read`, only if references cannot work.
43
40
 
44
41
  ## Login ladder
45
42
 
@@ -65,9 +62,9 @@ after one failure.
65
62
 
66
63
  - `saveTo`: `user` (Only me, reused later), `task` (throwaway, shared with this
67
64
  task's sessions) or `workspace` (team keys, schedules that run without you).
68
- - Name items after the service and account ("GitHub", "GitHub ops bot") and
69
- record `websites` and `username`.
70
- - Move an item with `secret_move` when a schedule or teammate needs it.
65
+ - Name items by service/account; record `websites` and `username`.
66
+ - Respect provider capabilities. Ask cards save to hosted vaults only.
67
+ Provider write and move limits: `references/troubleshooting.md`.
71
68
 
72
69
  ## Missing secrets and errors
73
70
 
@@ -75,12 +72,16 @@ A tool result with `secret_reference_error` ran nothing. For a missing secret
75
72
  it includes `askCard`: call `request_human_input` with it, changing only
76
73
  `saveTo` or `prompt`, then retry with the returned reference.
77
74
 
75
+ An unavailable or incomplete provider is not a missing secret: respect retry
76
+ times or reconnect/unlock it. Inspect unknown write outcomes before retrying;
77
+ never repeat a create just because its inventory refresh failed.
78
+
78
79
  ## Playbooks
79
80
 
80
- - `references/login-playbook.md`: browser sign-in, codes, CAPTCHAs.
81
- - `references/account-creation.md`: sign-ups and generated passwords.
82
- - `references/2fa-enrollment.md`: enroll and use your own TOTP.
83
- - `references/api-keys.md`: CLIs, env vars, config files, `secretfile`.
84
- - `references/ask-card.md`: the `kind: "secret"` question.
85
- - `references/troubleshooting.md`: every error code and what to do.
86
- - `references/codemode-and-scripts.md`: escaping in `ogtool` and scripts.
81
+ - Login: `references/login-playbook.md`.
82
+ - Sign-up: `references/account-creation.md`.
83
+ - TOTP: `references/2fa-enrollment.md`.
84
+ - CLI/files: `references/api-keys.md`.
85
+ - Secret card: `references/ask-card.md`.
86
+ - Errors: `references/troubleshooting.md`.
87
+ - Escaping: `references/codemode-and-scripts.md`.
@@ -23,6 +23,21 @@ and a `hint`. Fix the reference and retry the same action.
23
23
 
24
24
  ## Save and vault errors
25
25
 
26
+ Connected providers advertise their supported operations. Do not work around a
27
+ read-only or unsupported operation by exposing values in logs or process
28
+ arguments. A move requires recoverable removal at the source; a copy may still
29
+ be supported without it. Ask cards save to hosted vaults only.
30
+
31
+ An offline, locked, rate-limited or incomplete source is not proof that a secret
32
+ is missing. Check search `complete` and source freshness, preserve filters while
33
+ paging, and request `refresh: true` deliberately. It respects rate limits. Report
34
+ the reconnect or unlock action rather than asking for the secret again.
35
+
36
+ For `provider_outcome_unknown`, inspect the provider before retrying: a write
37
+ may already have succeeded. `provider_inventory_incomplete` with `saved: true`
38
+ means the write succeeded but the subsequent inventory refresh failed. Refresh
39
+ the inventory; never automatically repeat the create.
40
+
26
41
  | Error | What to do |
27
42
  |---|---|
28
43
  | `vault_read_only` | A connected vault is read-only. Save to an Opengeni vault (`saveTo` without `vault`) or ask the user to make it writable. |
@@ -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;