@opengeni/api-router 4.3.3-canary.39786771332001 → 4.5.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.
@@ -5,13 +5,23 @@ export type SessionListHint = {
5
5
  available: boolean;
6
6
  /** null means reconcile all loaded scopes, including formerly visible rows. */
7
7
  sessionIds: string[] | null;
8
+ /**
9
+ * The viewer's changed Site conversations, which ordinary lists omit. Only
10
+ * a surface that shows them (the rail's Site rows) needs to act on these.
11
+ */
12
+ siteSessionIds?: string[];
13
+ };
14
+ /** Authorized hint IDs; a plain array has no Site conversations. */
15
+ export type SessionListHintIds = string[] | {
16
+ sessionIds: string[];
17
+ siteSessionIds: string[];
8
18
  };
9
19
  /** Transient commit hints; only the subsequent authorized list read is truth. */
10
20
  export declare function subscribeSessionListHints(options: {
11
21
  source: SessionListChangeSource;
12
22
  workspaceId: string;
13
23
  subjectId: string;
14
- authorizeIds: (ids: string[]) => Promise<string[]>;
24
+ authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
15
25
  write: (hint: SessionListHint) => Promise<unknown>;
16
26
  fail: (error: unknown) => void;
17
27
  intervalMs: number;
@@ -2,6 +2,7 @@ import { type SessionEvent } from "@opengeni/contracts";
2
2
  import { type Database, type SessionListChangeSource } from "@opengeni/db";
3
3
  import { type EventBus } from "@opengeni/events";
4
4
  import type { Observability } from "@opengeni/observability";
5
+ import { type SessionListHintIds } from "./session-list-hints.js";
5
6
  export declare const SSE_QUEUED_FRAME_MAX_COUNT = 1;
6
7
  export declare const SSE_WRITE_STALL_TIMEOUT_MS = 30000;
7
8
  export declare const SSE_HEARTBEAT_INTERVAL_MS = 15000;
@@ -78,7 +79,7 @@ export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
78
79
  sessionLists?: {
79
80
  source: SessionListChangeSource;
80
81
  subjectId: string;
81
- authorizeIds: (ids: string[]) => Promise<string[]>;
82
+ authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
82
83
  } | undefined;
83
84
  };
84
85
  /**
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  startSlackInteractionPump,
21
21
  startTemporalScheduleCleanupPump,
22
22
  withAccessGrantSessionRlsContext
23
- } from "./chunk-S5VJ3GVR.js";
23
+ } from "./chunk-LXUAR3HS.js";
24
24
  import "./chunk-OGQDXVXY.js";
25
25
 
26
26
  // src/index.ts
@@ -0,0 +1,21 @@
1
+ import { type UserSetupBlocker } from "@opengeni/contracts";
2
+ export { sessionChatUrl } from "@opengeni/contracts";
3
+ type ToolRefusal = {
4
+ code?: string | undefined;
5
+ reason?: string | undefined;
6
+ };
7
+ export type UserSetupCardPost = (blocker: UserSetupBlocker) => Promise<{
8
+ eventId: string | null;
9
+ chatUrl: string | null;
10
+ } | null>;
11
+ /**
12
+ * Post the setup card when a tool was refused for something the person can fix
13
+ * themselves (USER_SETUP_BLOCKERS), and tell the agent it is posted. Every
14
+ * other result is returned unchanged. Posting is best effort: if it cannot be
15
+ * posted, the original refusal still stands.
16
+ */
17
+ export declare function withUserSetupCard<T extends ToolRefusal>(result: T, post: UserSetupCardPost): Promise<T & {
18
+ card?: "shown";
19
+ cardEventId?: string | null;
20
+ chatUrl?: string;
21
+ }>;
@@ -121,6 +121,7 @@ export declare const ClientSignalReport: z.ZodDiscriminatedUnion<[z.ZodObject<{
121
121
  settings: "settings";
122
122
  "setup-account": "setup-account";
123
123
  "site-auth": "site-auth";
124
+ sites: "sites";
124
125
  state: "state";
125
126
  "variable-sets": "variable-sets";
126
127
  }>;
@@ -19,6 +19,7 @@ export declare function sessionTenancyHttpError(error: unknown): Error;
19
19
  export declare function sessionListQuery(query: Record<string, string>, allowCursor?: boolean): {
20
20
  originSiteId: string | undefined;
21
21
  siteAccessId: string | undefined;
22
+ siteConversations: "include" | "group" | undefined;
22
23
  limit: string | undefined;
23
24
  parentSessionId: string | null | undefined;
24
25
  cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "4.3.3-canary.39786771332001",
3
+ "version": "4.5.0",
4
4
  "description": "Opengeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -45,29 +45,29 @@
45
45
  "@keeper-security/secrets-manager-core": "17.6.0",
46
46
  "@llamaindex/liteparse": "2.14.2",
47
47
  "@modelcontextprotocol/sdk": "^1.29.0",
48
- "@opengeni/agent-proto": "4.3.3-canary.39786771332001",
49
- "@opengeni/artifact-tool": "4.3.3-canary.39786771332001",
50
- "@opengeni/capabilities": "4.3.3-canary.39786771332001",
51
- "@opengeni/codemode": "4.3.3-canary.39786771332001",
52
- "@opengeni/codex": "4.3.3-canary.39786771332001",
53
- "@opengeni/config": "4.3.3-canary.39786771332001",
54
- "@opengeni/contracts": "4.3.3-canary.39786771332001",
55
- "@opengeni/core": "4.3.3-canary.39786771332001",
56
- "@opengeni/db": "4.3.3-canary.39786771332001",
57
- "@opengeni/documents": "4.3.3-canary.39786771332001",
58
- "@opengeni/events": "4.3.3-canary.39786771332001",
59
- "@opengeni/github": "4.3.3-canary.39786771332001",
60
- "@opengeni/interaction": "4.3.3-canary.39786771332001",
61
- "@opengeni/network": "4.3.3-canary.39786771332001",
62
- "@opengeni/observability": "4.3.3-canary.39786771332001",
63
- "@opengeni/runtime": "4.3.3-canary.39786771332001",
64
- "@opengeni/storage": "4.3.3-canary.39786771332001",
65
- "@opengeni/tool-gateway": "4.3.3-canary.39786771332001",
66
- "@opengeni/vaults": "4.3.3-canary.39786771332001",
67
- "@opengeni/vaults-keeper": "4.3.3-canary.39786771332001",
68
- "@opengeni/vaults-onepassword": "4.3.3-canary.39786771332001",
69
- "@opengeni/vaults-protonpass": "4.3.3-canary.39786771332001",
70
- "@opengeni/xai-subscription": "4.3.3-canary.39786771332001",
48
+ "@opengeni/agent-proto": "4.5.0",
49
+ "@opengeni/artifact-tool": "4.5.0",
50
+ "@opengeni/capabilities": "4.5.0",
51
+ "@opengeni/codemode": "4.5.0",
52
+ "@opengeni/codex": "4.5.0",
53
+ "@opengeni/config": "4.5.0",
54
+ "@opengeni/contracts": "4.5.0",
55
+ "@opengeni/core": "4.5.0",
56
+ "@opengeni/db": "4.5.0",
57
+ "@opengeni/documents": "4.5.0",
58
+ "@opengeni/events": "4.5.0",
59
+ "@opengeni/github": "4.5.0",
60
+ "@opengeni/interaction": "4.5.0",
61
+ "@opengeni/network": "4.5.0",
62
+ "@opengeni/observability": "4.5.0",
63
+ "@opengeni/runtime": "4.5.0",
64
+ "@opengeni/storage": "4.5.0",
65
+ "@opengeni/tool-gateway": "4.5.0",
66
+ "@opengeni/vaults": "4.5.0",
67
+ "@opengeni/vaults-keeper": "4.5.0",
68
+ "@opengeni/vaults-onepassword": "4.5.0",
69
+ "@opengeni/vaults-protonpass": "4.5.0",
70
+ "@opengeni/xai-subscription": "4.5.0",
71
71
  "@temporalio/client": "^1.17.0",
72
72
  "ajv": "^8.20.0",
73
73
  "better-auth": "^1.6.14",
package/src/app.ts CHANGED
@@ -193,6 +193,7 @@ import {
193
193
  registerProductUsageMetricBaselines,
194
194
  stampPersonalApiKeyRequest,
195
195
  verifiedDelegatedHumanAuthorizationForRequest,
196
+ browserExtension,
196
197
  } from "@opengeni/core";
197
198
  import { createManagedAuthNewSignupsGate } from "./auth/new-signups-gate";
198
199
  import {
@@ -1516,6 +1517,7 @@ export function createAppComposition(deps: AppDependencies): {
1516
1517
  deploymentRevision: deps.settings.deploymentRevision,
1517
1518
  claudeSubscriptionEnabled: deps.settings.claudeSubscriptionEnabled,
1518
1519
  connectedMachines: deps.settings.sandboxSelfhostedEnabled,
1520
+ browserExtension: browserExtension(deps.settings),
1519
1521
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
1520
1522
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
1521
1523
  defaultModel,
@@ -6,14 +6,22 @@ export type SessionListHint = {
6
6
  available: boolean;
7
7
  /** null means reconcile all loaded scopes, including formerly visible rows. */
8
8
  sessionIds: string[] | null;
9
+ /**
10
+ * The viewer's changed Site conversations, which ordinary lists omit. Only
11
+ * a surface that shows them (the rail's Site rows) needs to act on these.
12
+ */
13
+ siteSessionIds?: string[];
9
14
  };
10
15
 
16
+ /** Authorized hint IDs; a plain array has no Site conversations. */
17
+ export type SessionListHintIds = string[] | { sessionIds: string[]; siteSessionIds: string[] };
18
+
11
19
  /** Transient commit hints; only the subsequent authorized list read is truth. */
12
20
  export function subscribeSessionListHints(options: {
13
21
  source: SessionListChangeSource;
14
22
  workspaceId: string;
15
23
  subjectId: string;
16
- authorizeIds: (ids: string[]) => Promise<string[]>;
24
+ authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
17
25
  write: (hint: SessionListHint) => Promise<unknown>;
18
26
  fail: (error: unknown) => void;
19
27
  intervalMs: number;
@@ -24,12 +32,13 @@ export function subscribeSessionListHints(options: {
24
32
  let all = false;
25
33
  const ids = new Set<string>();
26
34
  let tail = Promise.resolve();
27
- const emit = (available: boolean, sessionIds: string[] | null) =>
35
+ const emit = (available: boolean, sessionIds: string[] | null, siteSessionIds: string[] = []) =>
28
36
  options.write({
29
37
  type: "workspace.sessions.changed",
30
38
  workspaceId: options.workspaceId,
31
39
  available,
32
40
  sessionIds,
41
+ ...(siteSessionIds.length ? { siteSessionIds } : {}),
33
42
  });
34
43
  const enqueue = (action: () => Promise<unknown>) => {
35
44
  tail = tail
@@ -73,10 +82,14 @@ export function subscribeSessionListHints(options: {
73
82
  ids.clear();
74
83
  enqueue(async () => {
75
84
  if (observedGeneration !== generation || !connected) return;
76
- const visibleIds = reconcileAll ? null : await options.authorizeIds(pendingIds);
85
+ const authorized = reconcileAll ? null : await options.authorizeIds(pendingIds);
77
86
  if (observedGeneration !== generation || !connected) return;
78
- if (visibleIds !== null && !visibleIds.length) return;
79
- await emit(true, visibleIds);
87
+ if (authorized === null) return await emit(true, null);
88
+ const visible = Array.isArray(authorized)
89
+ ? { sessionIds: authorized, siteSessionIds: [] }
90
+ : authorized;
91
+ if (!visible.sessionIds.length && !visible.siteSessionIds.length) return;
92
+ await emit(true, visible.sessionIds, visible.siteSessionIds);
80
93
  });
81
94
  }, options.intervalMs);
82
95
  return () => {
package/src/http/sse.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  } from "@opengeni/events";
23
23
  import type { Observability } from "@opengeni/observability";
24
24
  import { MANAGED_AUTH_ACTOR_EPOCH_HEADER } from "@opengeni/core/managed-auth-session-sets";
25
- import { subscribeSessionListHints } from "./session-list-hints";
25
+ import { subscribeSessionListHints, type SessionListHintIds } from "./session-list-hints";
26
26
 
27
27
  const SESSION_REPLAY_PAGE_SIZE = 100;
28
28
  const SESSION_REPLAY_PAGE_MAX_BYTES = 8 * 1024 * 1024;
@@ -744,7 +744,7 @@ export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
744
744
  | {
745
745
  source: SessionListChangeSource;
746
746
  subjectId: string;
747
- authorizeIds: (ids: string[]) => Promise<string[]>;
747
+ authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
748
748
  }
749
749
  | undefined;
750
750
  };
@@ -6407,6 +6407,15 @@ export const ACTION_CATALOG: readonly ActionCatalogEntry[] = [
6407
6407
  "PersonalGitHubOAuthStartResponse"
6408
6408
  ]
6409
6409
  },
6410
+ {
6411
+ "id": "recordSiteOpened",
6412
+ "method": "POST",
6413
+ "path": "/v1/workspaces/:workspaceId/published-artifacts/:artifactId/opened",
6414
+ "request": [],
6415
+ "response": [
6416
+ "SiteOpenedResponse"
6417
+ ]
6418
+ },
6410
6419
  {
6411
6420
  "id": "recoverSandbox",
6412
6421
  "method": "POST",
package/src/mcp/server.ts CHANGED
@@ -18,11 +18,7 @@ import { enableMachineScreenControl, enrollmentAccess } from "../sandbox/screen-
18
18
  import { registerNotificationTools } from "./notification-tools";
19
19
  import { excelAddinSetup, registerExcelTools } from "./excel-tools";
20
20
  import { capabilityAccountReadiness } from "./capability-account-readiness";
21
- import {
22
- sessionChatUrl,
23
- withMachinePermissionCard,
24
- type MachinePermissionCardPost,
25
- } from "./machine-permission-card";
21
+ import { sessionChatUrl, withUserSetupCard, type UserSetupCardPost } from "./user-setup-card";
26
22
  import {
27
23
  prepareWorkspaceArtifactUpload,
28
24
  prepareWorkspaceArtifactPublication,
@@ -214,8 +210,9 @@ import {
214
210
  buildCapabilityCatalog,
215
211
  CONNECTED_MACHINE_CAPABILITY_ID,
216
212
  EXCEL_ADDIN_CAPABILITY_ID,
213
+ browserExtension,
217
214
  nativeConnectionCapabilityRecommendations,
218
- OPENGENI_BROWSER_EXTENSION_URL,
215
+ userSetupCardPayload,
219
216
  requireLiveAgentAttemptAuthorization,
220
217
  requireSessionAuthorization,
221
218
  requireSessionAuthorizationListScope,
@@ -2300,7 +2297,7 @@ function registerSandboxFileArtifactTool(
2300
2297
  "sandbox_file_publish",
2301
2298
  {
2302
2299
  description:
2303
- "Publish one exact file from this session's /workspace into durable workspace storage. Use this before presenting a video, ZIP, CSV, JSON, Markdown, HTML, PDF, Office file, or other sandbox output as downloadable. Present the returned artifact.artifactId as [Open file](artifact:<artifactId>), or ![Preview](artifact:<artifactId>) for inline image/video/audio/PDF previews. Replace <artifactId> with the exact returned ID and use a descriptive label. Never expose a sandbox: URL as the durable result. The artifactId is also the file's workspace File ID, so tools that take workspace file IDs, such as a browser upload action, accept it. A new file gets this chat's visibility (Only me in an Only me chat, otherwise the workspace) unless you pass visibility; to change an existing file, such as a generated image, pass its artifactId and visibility instead of path. Check the result's access.whoCanOpen before sharing a link with anyone.",
2300
+ "Publish one file from this session's workspace so the person can open it. Use it for every file you hand over: document, spreadsheet, PDF, image, video, archive or data. Link the result as [Descriptive label](artifact:<artifactId>), or ![Descriptive label](artifact:<artifactId>) to preview an image, video, audio or PDF inline; never give a sandbox: link instead. The artifactId is also the file's workspace file ID, which tools such as a browser upload accept. A new file gets this chat's visibility unless you pass visibility. To change who can open an existing file, such as a generated image, pass its artifactId and visibility instead of path. Check access.whoCanOpen in the result before sharing the link with anyone else.",
2304
2301
  inputSchema: {
2305
2302
  path: z4.string().min(1).max(4_096).optional(),
2306
2303
  artifactId: z4.string().uuid().optional(),
@@ -4323,23 +4320,23 @@ function registerFleetTools(
4323
4320
  };
4324
4321
  };
4325
4322
 
4326
- // A refusal that only the owner's permission can resolve posts the Connected
4327
- // Machine card right away, in the agent's own turn, so the person can allow
4328
- // the machine without the agent having to find and call the card tool. Only
4329
- // exact agent attempts post it. The card names the machine the agent asked
4330
- // for, so it can lead with allowing that machine. A repeated refusal for the
4331
- // same machine in the same turn reuses that turn's card; the lookup and
4332
- // append run atomically under the attempt fence so parallel refusals cannot
4333
- // stack copies. Cards for every other capability are never limited.
4334
- const machinePermissionCard =
4335
- (toolName: string, target: string): MachinePermissionCardPost =>
4336
- async () => {
4323
+ // A refusal the person can fix themselves (USER_SETUP_BLOCKERS, for example
4324
+ // the owner not having allowed this chat to use their machine yet) posts the
4325
+ // setup card right away, in the agent's own turn, so the person sees the fix
4326
+ // without the agent having to find and call the card tool. Only exact agent
4327
+ // attempts post it. A machine card names the machine the agent asked for, so
4328
+ // it can lead with allowing that machine. A repeated refusal for the same
4329
+ // machine in the same turn reuses that turn's card; the lookup and append run
4330
+ // atomically under the attempt fence so parallel refusals cannot stack
4331
+ // copies. Cards for every other capability are never limited.
4332
+ const userSetupCard =
4333
+ (toolName: string, target: string): UserSetupCardPost =>
4334
+ async (blocker) => {
4337
4335
  const requestedSandboxId = UUID_TARGET.test(target) ? target.toLowerCase() : null;
4338
4336
  if (!exactAgentAttemptClaims(grant)) return null;
4339
4337
  const claims = exactAgentCommandContext(grant, sessionId);
4340
- const item = nativeConnectionCapabilityRecommendations({ connectedMachines: true }).find(
4341
- (candidate) => candidate.id === CONNECTED_MACHINE_CAPABILITY_ID,
4342
- )!;
4338
+ const payload = userSetupCardPayload(blocker, { toolName, requestedSandboxId });
4339
+ if (!payload) return null;
4343
4340
  const appended = await appendTurnCapabilityCardOnce(
4344
4341
  deps.db,
4345
4342
  grant.workspaceId,
@@ -4347,23 +4344,13 @@ function registerFleetTools(
4347
4344
  claims.callerTurnId,
4348
4345
  claims.callerExecutionGeneration,
4349
4346
  claims.callerAttemptId,
4350
- item.id,
4351
- {
4352
- type: "tool.auth_needed",
4353
- payload: {
4354
- ...capabilityCardPayload(item, {
4355
- action: "connect",
4356
- rationale: "Allow this chat to use your computer.",
4357
- toolName,
4358
- }),
4359
- ...(requestedSandboxId ? { requestedSandboxId } : {}),
4360
- },
4361
- },
4347
+ blocker.capabilityId,
4348
+ { type: "tool.auth_needed", payload },
4362
4349
  requestedSandboxId,
4363
4350
  );
4364
4351
  await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, appended.events);
4365
4352
  if (!appended.accepted) {
4366
- throw new Error("The calling turn was replaced before the machine card committed.");
4353
+ throw new Error("The calling turn was replaced before the setup card committed.");
4367
4354
  }
4368
4355
  return {
4369
4356
  eventId: appended.existingEventId ?? appended.events[0]?.id ?? null,
@@ -4398,9 +4385,9 @@ function registerFleetTools(
4398
4385
  },
4399
4386
  async ({ target }) =>
4400
4387
  json(
4401
- await withMachinePermissionCard(
4388
+ await withUserSetupCard(
4402
4389
  await swapActiveSandbox(services, await fleetContext(), target),
4403
- machinePermissionCard("sandbox_attach", target),
4390
+ userSetupCard("sandbox_attach", target),
4404
4391
  ),
4405
4392
  ),
4406
4393
  );
@@ -4414,9 +4401,9 @@ function registerFleetTools(
4414
4401
  },
4415
4402
  async ({ target }) =>
4416
4403
  json(
4417
- await withMachinePermissionCard(
4404
+ await withUserSetupCard(
4418
4405
  await swapActiveSandbox(services, await fleetContext(), target),
4419
- machinePermissionCard("sandbox_swap", target),
4406
+ userSetupCard("sandbox_swap", target),
4420
4407
  ),
4421
4408
  ),
4422
4409
  );
@@ -4458,12 +4445,12 @@ function registerFleetTools(
4458
4445
  )
4459
4446
  : null;
4460
4447
  return json(
4461
- await withMachinePermissionCard(
4448
+ await withUserSetupCard(
4462
4449
  await runOnSandbox(services, await fleetContext(), target, typedOp, {
4463
4450
  ...(transientExecEnvironment ? { transientExecEnvironment } : {}),
4464
4451
  ...(secretResolver ? { secretResolver } : {}),
4465
4452
  }),
4466
- machinePermissionCard("run_on", target),
4453
+ userSetupCard("run_on", target),
4467
4454
  ),
4468
4455
  );
4469
4456
  },
@@ -4484,9 +4471,10 @@ function registerFleetTools(
4484
4471
  // Same live-attempt authority the other fleet tools resolve, without
4485
4472
  // requiring a session box: a chat with no sandbox can still connect one.
4486
4473
  await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
4487
- const item = nativeConnectionCapabilityRecommendations({ connectedMachines: true }).find(
4488
- (candidate) => candidate.id === CONNECTED_MACHINE_CAPABILITY_ID,
4489
- )!;
4474
+ const item = nativeConnectionCapabilityRecommendations({
4475
+ connectedMachines: true,
4476
+ browserExtension: browserExtension(deps.settings),
4477
+ }).find((candidate) => candidate.id === CONNECTED_MACHINE_CAPABILITY_ID)!;
4490
4478
  const setup = await capabilitySetupProjection(
4491
4479
  deps,
4492
4480
  grant.workspaceId,
@@ -4514,7 +4502,7 @@ function registerFleetTools(
4514
4502
  setup.status === "ready"
4515
4503
  ? `${setup.detail} The Connected Machine card lists them, so the person can choose one for this chat or connect another.`
4516
4504
  : "The Connected Machine card was posted. It shows the person what to do, so do not describe its buttons or steps; nothing is connected yet."
4517
- } Check sandboxes_list after they confirm. Chrome needs the browser extension (${OPENGENI_BROWSER_EXTENSION_URL}) on the connected machine.`,
4505
+ } Check sandboxes_list after they confirm. Chrome needs the browser extension (${browserExtension(deps.settings).installUrl}) on the connected machine.`,
4518
4506
  });
4519
4507
  }
4520
4508
  return json(
@@ -4544,7 +4532,7 @@ function registerConnectedMachineTools(
4544
4532
  {
4545
4533
  description:
4546
4534
  "Create a single-use Connected Machine enrollment token for this workspace (expires in one hour) using existing enrollments:manage authority. Returns the token, expiry and Unix/PowerShell install commands. Use it when you can run commands on the target machine yourself (run_on, SSH, a VM you control): run the matching command there, then verify the machine with sandboxes_list. One token connects one machine; mint another for each additional machine (the same machine may rerun its command). When the person must run it on their own computer, post the Connected Machine card instead (capability_authorization_request with api:connected-machine), which mints the command in their browser; do not paste a token into the chat. Screen control is optional and defaults off; to turn it on for a machine that is already connected, use connected_machine_enable_screen_control instead of reconnecting. The token grants whole-machine access on enrollment; do not put it in public code or unrelated logs. Existing machine connections are preserved. For Chrome, the person installs the Opengeni Browser extension (" +
4547
- OPENGENI_BROWSER_EXTENSION_URL +
4535
+ browserExtension(deps.settings).installUrl +
4548
4536
  ") after the machine is connected.",
4549
4537
  inputSchema: { allowScreenControl: z4.boolean().optional() },
4550
4538
  },
@@ -4576,7 +4564,7 @@ function registerConnectedMachineTools(
4576
4564
  {
4577
4565
  description:
4578
4566
  "Turn screen control on for a machine that is already connected, in place, so agents can see and use its screen (computer and attached-Chrome tools). `target` is a sandboxes_list id or an enrollment id. Needs no human click, token, terminal or reconnect: the platform records the consent on that machine and its agent refreshes its own credentials over the live connection. status=active means it is on now. status=pending means it is allowed and turns on by itself: reason=offline when the machine next connects; reason=agent_update_required after its agent is updated (update it with the machine update action, POST /v1/workspaces/{workspaceId}/machines/{enrollmentId}/update); reason=renewal_failed: call again shortly; reason=reconnect_required: the machine's connection predates in-place changes, so rerun the connect command on it (connected_machine_enroll_token with allowScreenControl=true). On a Mac the OS must also allow Screen Recording, Accessibility and Input Monitoring for the app that runs the agent (the name macOS lists in those panes): open each pane on the Mac with run_on exec `open 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'` (Privacy_Accessibility, Privacy_ListenEvent; also the privacy-settings machine action) and ask the person to switch that app on there, choosing Quit & Reopen if macOS asks; the machine list reports runtime.macPermissions. Chrome additionally needs the Opengeni Browser extension (" +
4579
- OPENGENI_BROWSER_EXTENSION_URL +
4567
+ browserExtension(deps.settings).installUrl +
4580
4568
  ") on that machine.",
4581
4569
  inputSchema: { target: z4.string().min(1) },
4582
4570
  },
@@ -6733,6 +6721,7 @@ function registerCapabilityDiscoveryTools(
6733
6721
  ...current.items,
6734
6722
  ...nativeConnectionCapabilityRecommendations({
6735
6723
  connectedMachines: deps.settings.sandboxSelfhostedEnabled,
6724
+ browserExtension: browserExtension(deps.settings),
6736
6725
  }),
6737
6726
  ];
6738
6727
  const ranked = searchCapabilityCatalogItems(candidates, query, limit ?? 8);
@@ -6806,6 +6795,7 @@ function registerCapabilityDiscoveryTools(
6806
6795
  ...current.items,
6807
6796
  ...nativeConnectionCapabilityRecommendations({
6808
6797
  connectedMachines: deps.settings.sandboxSelfhostedEnabled,
6798
+ browserExtension: browserExtension(deps.settings),
6809
6799
  }),
6810
6800
  ].find((candidate) => candidate.id === capabilityId);
6811
6801
  if (!item || !capabilityCatalogItemIsTrustedForExposure(item)) {
@@ -6845,7 +6835,7 @@ function registerCapabilityDiscoveryTools(
6845
6835
  capabilityId: item.id,
6846
6836
  status: "ready",
6847
6837
  eventId,
6848
- message: `${setup.detail} The Connected Machine card is in this chat: the person can choose a machine for this chat or connect another. Chrome needs the browser extension (${OPENGENI_BROWSER_EXTENSION_URL}) on a connected machine.`,
6838
+ message: `${setup.detail} The Connected Machine card is in this chat: the person can choose a machine for this chat or connect another. Chrome needs the browser extension (${browserExtension(deps.settings).installUrl}) on a connected machine.`,
6849
6839
  });
6850
6840
  }
6851
6841
  if (githubCardWhenReady) {
@@ -0,0 +1,42 @@
1
+ import {
2
+ type UserSetupBlocker,
3
+ userSetupBlocker,
4
+ userSetupCardShownMessage,
5
+ } from "@opengeni/contracts";
6
+
7
+ export { sessionChatUrl } from "@opengeni/contracts";
8
+
9
+ type ToolRefusal = { code?: string | undefined; reason?: string | undefined };
10
+
11
+ export type UserSetupCardPost = (blocker: UserSetupBlocker) => Promise<{
12
+ eventId: string | null;
13
+ chatUrl: string | null;
14
+ } | null>;
15
+
16
+ /**
17
+ * Post the setup card when a tool was refused for something the person can fix
18
+ * themselves (USER_SETUP_BLOCKERS), and tell the agent it is posted. Every
19
+ * other result is returned unchanged. Posting is best effort: if it cannot be
20
+ * posted, the original refusal still stands.
21
+ */
22
+ export async function withUserSetupCard<T extends ToolRefusal>(
23
+ result: T,
24
+ post: UserSetupCardPost,
25
+ ): Promise<T & { card?: "shown"; cardEventId?: string | null; chatUrl?: string }> {
26
+ const blocker = userSetupBlocker(result.code);
27
+ if (!blocker) return result;
28
+ let posted: Awaited<ReturnType<UserSetupCardPost>>;
29
+ try {
30
+ posted = await post(blocker);
31
+ } catch {
32
+ return result;
33
+ }
34
+ if (!posted) return result;
35
+ return {
36
+ ...result,
37
+ reason: userSetupCardShownMessage(blocker, posted.chatUrl),
38
+ card: "shown",
39
+ cardEventId: posted.eventId,
40
+ ...(posted.chatUrl ? { chatUrl: posted.chatUrl } : {}),
41
+ };
42
+ }
@@ -6126,6 +6126,26 @@ export function parseBrowserScreenshotOptions(
6126
6126
  export function browserTabDiagnosisError(error: unknown): unknown {
6127
6127
  if (!(error instanceof BrowserControlRequestError)) return error;
6128
6128
  const code = error.error.code;
6129
+ // A controller that says a resource is gone for good (for example, the
6130
+ // person's Chrome profile reconnected and this session no longer controls
6131
+ // it) answers 503 but not retryable. As a 503 it would reach the agent as
6132
+ // "temporarily unavailable, retry", and every retry fails the same way, so
6133
+ // keep the controller's guidance and present it as the conflict it is.
6134
+ if (
6135
+ code === "resource_unavailable" &&
6136
+ error.status >= 500 &&
6137
+ !error.retryable &&
6138
+ !legacyInteractionCapacityFailure(error, "browser")
6139
+ ) {
6140
+ const failure = new ApiHttpError(409, {
6141
+ code: "conflict",
6142
+ message: error.error.message,
6143
+ retryable: false,
6144
+ outcomeUnknown: false,
6145
+ });
6146
+ failure.cause = error;
6147
+ return failure;
6148
+ }
6129
6149
  if (code !== "tab_not_responding" && code !== "tab_crashed") return error;
6130
6150
  const failure = new ApiHttpError(409, {
6131
6151
  code,
@@ -40,7 +40,7 @@ import {
40
40
  listInteractionInterventions,
41
41
  listNetworkRoutes,
42
42
  listSiteAuthConnections,
43
- listVisibleSessionInvalidationIds,
43
+ listVisibleSessionInvalidationHints,
44
44
  sessionListChangesFor,
45
45
  resolveInteractionIntervention,
46
46
  updateNetworkRoute,
@@ -113,7 +113,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
113
113
  freshGrant,
114
114
  "http",
115
115
  );
116
- return await listVisibleSessionInvalidationIds(deps.db, workspaceId, ids, {
116
+ return await listVisibleSessionInvalidationHints(deps.db, workspaceId, ids, {
117
117
  subjectId: freshGrant.subjectId,
118
118
  ...(authorizationScope ? { authorizationScope } : {}),
119
119
  personalWorkspaceOwnerException:
@@ -1042,6 +1042,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1042
1042
  ...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
1043
1043
  ...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
1044
1044
  ...(query.siteAccessId ? { siteAccessId: query.siteAccessId } : {}),
1045
+ ...(query.siteConversations ? { siteConversations: query.siteConversations } : {}),
1045
1046
  ...(query.createdBy ? { createdBy: query.createdBy } : {}),
1046
1047
  ...(query.updatedFrom ? { updatedFrom: query.updatedFrom } : {}),
1047
1048
  ...(query.updatedBefore ? { updatedBefore: query.updatedBefore } : {}),
@@ -5891,6 +5892,7 @@ export function sessionListQuery(
5891
5892
  ): {
5892
5893
  originSiteId: string | undefined;
5893
5894
  siteAccessId: string | undefined;
5895
+ siteConversations: "include" | "group" | undefined;
5894
5896
  limit: string | undefined;
5895
5897
  parentSessionId: string | null | undefined;
5896
5898
  cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
@@ -5925,6 +5927,33 @@ export function sessionListQuery(
5925
5927
  });
5926
5928
  if (originSiteId !== undefined && !z.string().uuid().safeParse(originSiteId).success)
5927
5929
  throw new HTTPException(400, { message: "originSiteId must be a Site id" });
5930
+ const siteConversations = query.siteConversations;
5931
+ if (siteConversations !== undefined) {
5932
+ if (siteConversations !== "include" && siteConversations !== "group")
5933
+ throw new HTTPException(400, { message: 'siteConversations must be "include" or "group"' });
5934
+ if (!allowCursor)
5935
+ throw new HTTPException(400, { message: 'siteConversations requires view="page"' });
5936
+ // A Site's own history is already explicit; Site frames never reach this.
5937
+ if (originSiteId !== undefined || siteAccessId !== undefined)
5938
+ throw new HTTPException(400, {
5939
+ message: "siteConversations cannot be combined with a Site filter",
5940
+ });
5941
+ if (
5942
+ siteConversations === "group" &&
5943
+ (query.parentSessionId !== "null" || query.pinsOnly === "true")
5944
+ )
5945
+ throw new HTTPException(400, {
5946
+ message: 'siteConversations="group" requires a root page (parentSessionId=null)',
5947
+ });
5948
+ // Archived is a flat list: its Site conversations are ordinary rows.
5949
+ if (
5950
+ siteConversations === "group" &&
5951
+ (query.archiveStatus === "archived" || query.archivedOnly === "true")
5952
+ )
5953
+ throw new HTTPException(400, {
5954
+ message: 'siteConversations="group" cannot list archived sessions; use "include"',
5955
+ });
5956
+ }
5928
5957
  // "null" = roots only; a uuid = children of that session; anything else is
5929
5958
  // a client error (an unvalidated value would surface as a Postgres uuid cast
5930
5959
  // failure -> 500 rather than an honest 400).
@@ -6092,6 +6121,7 @@ export function sessionListQuery(
6092
6121
  limit: query.limit,
6093
6122
  originSiteId,
6094
6123
  siteAccessId,
6124
+ siteConversations,
6095
6125
  parentSessionId:
6096
6126
  parentSessionId === undefined
6097
6127
  ? undefined