@opengeni/api-router 1.4.5-canary.38533733713001 → 2.0.1

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/app.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  withDefaultEnabledCapabilityMcpTools,
29
29
  workflowIdForSession,
30
30
  workspaceActorContextExempt
31
- } from "./chunk-D5WJ24GY.js";
31
+ } from "./chunk-ZDTWXRFX.js";
32
32
  import "./chunk-OGQDXVXY.js";
33
33
  export {
34
34
  API_MAX_REQUEST_BODY_BYTES,
@@ -0,0 +1,11 @@
1
+ import type { AccessGrant } from "@opengeni/contracts";
2
+ import { type ApiRouteDeps } from "@opengeni/core";
3
+ import type { AttachedBrowserViewer } from "@opengeni/db";
4
+ /**
5
+ * The person whose machine access decides which attached browsers a caller
6
+ * may see and control. Personal (Only me) machines belong to one person, so a
7
+ * human sees and controls their own; an agent acts for the person who started
8
+ * its turn (never merely for the workspace). Other callers (keys, services)
9
+ * reach only machines that are not personal.
10
+ */
11
+ export declare function attachedBrowserViewer(deps: Pick<ApiRouteDeps, "db">, grant: AccessGrant, workspaceId: string): Promise<AttachedBrowserViewer>;
@@ -4,3 +4,9 @@ import { type ManagedAuth } from "@opengeni/core";
4
4
  /** Server-only proof for a product-owned endpoint; never returned to a browser. */
5
5
  export declare function managedAuthSelectedProofHeaders(auth: ManagedAuth, token: string): Promise<Headers>;
6
6
  export declare function createBetterAuthSessionAdapter(auth: ManagedAuth, db: Database): ManagedAuthSessionAdapter;
7
+ /**
8
+ * The provider's own session cookie for an exact session token (or its
9
+ * expiry when `token` is null), plus invalidation of stale provider cache
10
+ * cookies, exactly as a browser sign-in would set them.
11
+ */
12
+ export declare function managedAuthSessionCookieHeaders(auth: ManagedAuth, token: string | null, currentCookieHeader?: string | null): Promise<string[]>;
@@ -385,7 +385,7 @@ async function coreCodexConnected(c, deps, input) {
385
385
  );
386
386
  default:
387
387
  throw new HTTPException3(403, {
388
- message: "only an organization administrator can connect a new shared Codex account; workspace administrators can reconnect the accounts their workspace manages"
388
+ message: input.workspaceId === null ? "only an organization administrator can connect a Codex account for the whole organization" : "only an administrator of this workspace or of the organization can connect a Codex account here"
389
389
  });
390
390
  }
391
391
  }
@@ -407,7 +407,7 @@ async function coreCodexConnected(c, deps, input) {
407
407
  function disconnectRefused(outcome) {
408
408
  if (outcome === "forbidden") {
409
409
  throw new HTTPException3(403, {
410
- message: "only an organization administrator can disconnect a shared Codex account"
410
+ message: "only an organization administrator can disconnect this Codex account; a workspace administrator can disconnect only an account that serves just their workspace"
411
411
  });
412
412
  }
413
413
  if (outcome === "unresolved_redemption") {
@@ -2723,7 +2723,7 @@ function coreConnectRefused(reason, displayName, personal) {
2723
2723
  });
2724
2724
  default:
2725
2725
  return new HTTPException7(personal ? 409 : 403, {
2726
- message: personal ? `a private ${displayName} account is connected from your Personal workspace` : `only an organization administrator can connect a new shared ${displayName} account; workspace administrators can reconnect the accounts their workspace manages`
2726
+ message: personal ? `a private ${displayName} account is connected from your Personal workspace` : `only an administrator of this workspace or of the organization can connect a ${displayName} account here`
2727
2727
  });
2728
2728
  }
2729
2729
  }
@@ -2732,7 +2732,7 @@ function disconnectRefused2(outcome, displayName) {
2732
2732
  throw new HTTPException7(404, { message: `${displayName} account not found` });
2733
2733
  if (outcome === "forbidden")
2734
2734
  throw new HTTPException7(403, {
2735
- message: `only an organization administrator can disconnect a shared ${displayName} account`
2735
+ message: `only an organization administrator can disconnect this ${displayName} account; a workspace administrator can disconnect only an account that serves just their workspace`
2736
2736
  });
2737
2737
  if (outcome === "unresolved_redemption")
2738
2738
  throw new HTTPException7(409, {
@@ -9521,7 +9521,7 @@ function registerVaultProviderRoutes(app, deps, options = {}) {
9521
9521
  }
9522
9522
  const created = await provider.createVault(routeProviderContext(connectionId), {
9523
9523
  name: payload.name,
9524
- description: "Managed by OpenGeni",
9524
+ description: "Managed by Opengeni",
9525
9525
  allowAdminsAccess: true,
9526
9526
  ...payload.groupId ? { groupId: payload.groupId } : {}
9527
9527
  });
@@ -13869,29 +13869,26 @@ async function startMcpOAuthWithinDeadline(deps, context, deadline) {
13869
13869
  ...existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}
13870
13870
  });
13871
13871
  const expiresAt = new Date(Date.now() + oauthStateTtlMs);
13872
- let state = fullState;
13873
- if (settings.integrationsOauthShortStateEnabled) {
13874
- const stateId = randomUUID();
13875
- state = createSignedState2(requireIntegrationsStateSecret(settings), {
13876
- kind: "mcp_oauth_reference",
13872
+ const stateId = randomUUID();
13873
+ const state = createSignedState2(requireIntegrationsStateSecret(settings), {
13874
+ kind: "mcp_oauth_reference",
13875
+ id: stateId,
13876
+ accountId: context.accountId,
13877
+ workspaceId: context.workspaceId
13878
+ });
13879
+ if (state.length > 512) {
13880
+ throw new Error("MCP OAuth reference state exceeded its provider-safe length");
13881
+ }
13882
+ await deadline.run(
13883
+ "state_persist",
13884
+ async () => storeIntegrationOAuthPendingState(db, {
13877
13885
  id: stateId,
13878
13886
  accountId: context.accountId,
13879
- workspaceId: context.workspaceId
13880
- });
13881
- if (state.length > 512) {
13882
- throw new Error("MCP OAuth reference state exceeded its provider-safe length");
13883
- }
13884
- await deadline.run(
13885
- "state_persist",
13886
- async () => storeIntegrationOAuthPendingState(db, {
13887
- id: stateId,
13888
- accountId: context.accountId,
13889
- workspaceId: context.workspaceId,
13890
- stateEncrypted: encryptEnvironmentValue4(key2, fullState),
13891
- expiresAt
13892
- })
13893
- );
13894
- }
13887
+ workspaceId: context.workspaceId,
13888
+ stateEncrypted: encryptEnvironmentValue4(key2, fullState),
13889
+ expiresAt
13890
+ })
13891
+ );
13895
13892
  const authorizationUrl = buildAuthorizationUrl({
13896
13893
  endpoint: discovery.as.authorizationEndpoint,
13897
13894
  settings,
@@ -36115,7 +36112,7 @@ var RULES = [
36115
36112
  {
36116
36113
  method: "PATCH",
36117
36114
  pattern: /\/(codex|claude|supergrok)\/accounts\/:accountId\/allocator$/,
36118
- text: ({ provider }) => `Turn "Use for new work" on or off for a ${provider} account (pause or resume it). Paused accounts aren't picked for new chats or schedules; running work continues. Send the current allocator version.`
36115
+ text: ({ provider }) => `Turn "Enable account" on or off for a ${provider} account (enable or disable it, sometimes called pause or resume). Disabled accounts aren't picked for new chats or schedules; running work continues. Send the current allocator version.`
36119
36116
  },
36120
36117
  {
36121
36118
  method: "PATCH",
@@ -39884,7 +39881,7 @@ function registerFleetTools(server, deps, grant, sessionId, json2) {
39884
39881
  server.registerTool(
39885
39882
  "sandbox_provision",
39886
39883
  {
39887
- description: "Provision a new sandbox for the fleet. kind=selfhosted posts the Connected Machine card in this chat: the person copies a one-line connect command, runs it on their own computer, and picks Use in this chat; the card also offers the OpenGeni Browser Chrome extension once the machine is connected. When you can already run commands on the target machine, install it yourself with connected_machine_enroll_token instead. kind=modal creates a named Modal sandbox record, but it is NOT yet attachable as a swap target: routing a session onto a second Modal box is not supported yet, so sandbox_swap to its id is rejected. Use the session's own box (the default) or attach a Connected Machine instead.",
39884
+ description: "Provision a new sandbox for the fleet. kind=selfhosted posts the Connected Machine card in this chat: the person copies a one-line connect command, runs it on their own computer, and picks Use in this chat; the card also offers the Opengeni Browser Chrome extension once the machine is connected. When you can already run commands on the target machine, install it yourself with connected_machine_enroll_token instead. kind=modal creates a named Modal sandbox record, but it is NOT yet attachable as a swap target: routing a session onto a second Modal box is not supported yet, so sandbox_swap to its id is rejected. Use the session's own box (the default) or attach a Connected Machine instead.",
39888
39885
  inputSchema: {
39889
39886
  kind: z45.enum(["selfhosted", "modal"]),
39890
39887
  name: z45.string().min(1).max(120).optional()
@@ -39919,7 +39916,7 @@ function registerFleetTools(server, deps, grant, sessionId, json2) {
39919
39916
  kind: "selfhosted",
39920
39917
  status: "card_posted",
39921
39918
  eventId,
39922
- message: `${setup.status === "ready" ? `${setup.detail} The Connected Machine card lists them with Use in this chat and can connect another.` : "The Connected Machine card was posted. The person copies its one-line connect command, runs it on their machine, and picks Use in this chat; nothing is connected yet."} Check sandboxes_list after they confirm. Chrome needs the OpenGeni Browser extension (${OPENGENI_BROWSER_EXTENSION_URL}) on the connected machine.`
39919
+ message: `${setup.status === "ready" ? `${setup.detail} The Connected Machine card lists them with Use in this chat and can connect another.` : "The Connected Machine card was posted. The person copies its one-line connect command, runs it on their machine, and picks Use in this chat; nothing is connected yet."} Check sandboxes_list after they confirm. Chrome needs the Opengeni Browser extension (${OPENGENI_BROWSER_EXTENSION_URL}) on the connected machine.`
39923
39920
  });
39924
39921
  }
39925
39922
  return json2(
@@ -39935,7 +39932,7 @@ function registerConnectedMachineTools(server, deps, grant, sessionId, json2) {
39935
39932
  server.registerTool(
39936
39933
  "connected_machine_enroll_token",
39937
39934
  {
39938
- description: "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 (" + OPENGENI_BROWSER_EXTENSION_URL + ") after the machine is connected.",
39935
+ description: "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 (" + OPENGENI_BROWSER_EXTENSION_URL + ") after the machine is connected.",
39939
39936
  inputSchema: { allowScreenControl: z45.boolean().optional() }
39940
39937
  },
39941
39938
  async ({ allowScreenControl }) => {
@@ -39963,7 +39960,7 @@ function registerConnectedMachineTools(server, deps, grant, sessionId, json2) {
39963
39960
  server.registerTool(
39964
39961
  "connected_machine_enable_screen_control",
39965
39962
  {
39966
- description: "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: OpenGeni 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 OpenGeni: 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 OpenGeni on there, choosing Quit & Reopen if macOS asks; the machine list reports runtime.macPermissions. Chrome additionally needs the OpenGeni Browser extension (" + OPENGENI_BROWSER_EXTENSION_URL + ") on that machine.",
39963
+ description: "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: Opengeni 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 Opengeni: 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 Opengeni on there, choosing Quit & Reopen if macOS asks; the machine list reports runtime.macPermissions. Chrome additionally needs the Opengeni Browser extension (" + OPENGENI_BROWSER_EXTENSION_URL + ") on that machine.",
39967
39964
  inputSchema: { target: z45.string().min(1) }
39968
39965
  },
39969
39966
  async ({ target }) => {
@@ -41817,7 +41814,7 @@ function registerCapabilityDiscoveryTools(server, deps, grant, sessionId, json2)
41817
41814
  server.registerTool(
41818
41815
  "capability_authorization_request",
41819
41816
  {
41820
- description: "Show a Connect card in this chat for a suitable capability returned by capability_catalog_search with setup.nextAction. Supply its capability ID and a brief rationale explaining how it helps the task; no separate confirmation is needed before showing the card. Requesting setup needs no integration-management permission and grants no access. The authenticated human completes setup through the card; never ask them to paste credentials into chat. Do not request another card for the same pending setup, or for a candidate reported ready or unavailable. GitHub App (api:github-app) and Connected Machine (api:connected-machine) are exceptions: when already connected their card is still shown, listing repositories or machines the person can use in this chat. Use api:connected-machine when the person should connect their own computer: the card gives them a one-line connect command and the OpenGeni Browser Chrome extension link; never paste an enroll token into the chat yourself." + (secretsSelected ? ' For a single password, API key or code for a service with no connector here, ask with a kind: "secret" question in request_human_input instead; you get a saved reference back.' : ""),
41817
+ description: "Show a Connect card in this chat for a suitable capability returned by capability_catalog_search with setup.nextAction. Supply its capability ID and a brief rationale explaining how it helps the task; no separate confirmation is needed before showing the card. Requesting setup needs no integration-management permission and grants no access. The authenticated human completes setup through the card; never ask them to paste credentials into chat. Do not request another card for the same pending setup, or for a candidate reported ready or unavailable. GitHub App (api:github-app) and Connected Machine (api:connected-machine) are exceptions: when already connected their card is still shown, listing repositories or machines the person can use in this chat. Use api:connected-machine when the person should connect their own computer: the card gives them a one-line connect command and the Opengeni Browser Chrome extension link; never paste an enroll token into the chat yourself." + (secretsSelected ? ' For a single password, API key or code for a service with no connector here, ask with a kind: "secret" question in request_human_input instead; you get a saved reference back.' : ""),
41821
41818
  inputSchema: {
41822
41819
  capabilityId: z45.string().min(1).max(512),
41823
41820
  rationale: z45.string().min(1).max(2e3)
@@ -41863,7 +41860,7 @@ function registerCapabilityDiscoveryTools(server, deps, grant, sessionId, json2)
41863
41860
  capabilityId: item.id,
41864
41861
  status: "ready",
41865
41862
  eventId,
41866
- message: `${setup.detail} The Connected Machine card is in this chat: the person can pick a machine with "Use in this chat" or connect another. Chrome needs the OpenGeni Browser extension (${OPENGENI_BROWSER_EXTENSION_URL}) on a connected machine.`
41863
+ message: `${setup.detail} The Connected Machine card is in this chat: the person can pick a machine with "Use in this chat" or connect another. Chrome needs the Opengeni Browser extension (${OPENGENI_BROWSER_EXTENSION_URL}) on a connected machine.`
41867
41864
  });
41868
41865
  }
41869
41866
  if (githubCardWhenReady) {
@@ -51080,31 +51077,7 @@ function createBetterAuthSessionAdapter(auth, db) {
51080
51077
  await db.execute(sql`delete from auth_sessions where id = ${input.authSessionId}`);
51081
51078
  },
51082
51079
  async createLegacySelectedSessionCookies(input, currentCookieHeader) {
51083
- const context = await auth.$context;
51084
- const cookie = context.authCookies.sessionToken;
51085
- const value = input ? signedCookieValue(input.token, context.secret) : "";
51086
- const headers = [
51087
- serializeCookieHeader(cookie.name, value, {
51088
- ...cookie.attributes,
51089
- ...input ? {} : { maxAge: 0, expires: /* @__PURE__ */ new Date(0) }
51090
- })
51091
- ];
51092
- for (const cacheCookie of [
51093
- context.authCookies.sessionData,
51094
- context.authCookies.accountData,
51095
- context.authCookies.dontRememberToken
51096
- ]) {
51097
- for (const name of cacheCookieNames(cacheCookie.name, currentCookieHeader)) {
51098
- headers.push(
51099
- serializeCookieHeader(name, "", {
51100
- ...cacheCookie.attributes,
51101
- maxAge: 0,
51102
- expires: /* @__PURE__ */ new Date(0)
51103
- })
51104
- );
51105
- }
51106
- }
51107
- return headers;
51080
+ return await managedAuthSessionCookieHeaders(auth, input?.token ?? null, currentCookieHeader);
51108
51081
  }
51109
51082
  };
51110
51083
  }
@@ -51116,6 +51089,33 @@ function liveResolvedSession(value) {
51116
51089
  if (!Number.isFinite(expiryMillis) || expiryMillis <= Date.now()) return null;
51117
51090
  return value;
51118
51091
  }
51092
+ async function managedAuthSessionCookieHeaders(auth, token, currentCookieHeader) {
51093
+ const context = await auth.$context;
51094
+ const cookie = context.authCookies.sessionToken;
51095
+ const value = token ? signedCookieValue(token, context.secret) : "";
51096
+ const headers = [
51097
+ serializeCookieHeader(cookie.name, value, {
51098
+ ...cookie.attributes,
51099
+ ...token ? {} : { maxAge: 0, expires: /* @__PURE__ */ new Date(0) }
51100
+ })
51101
+ ];
51102
+ for (const cacheCookie of [
51103
+ context.authCookies.sessionData,
51104
+ context.authCookies.accountData,
51105
+ context.authCookies.dontRememberToken
51106
+ ]) {
51107
+ for (const name of cacheCookieNames(cacheCookie.name, currentCookieHeader)) {
51108
+ headers.push(
51109
+ serializeCookieHeader(name, "", {
51110
+ ...cacheCookie.attributes,
51111
+ maxAge: 0,
51112
+ expires: /* @__PURE__ */ new Date(0)
51113
+ })
51114
+ );
51115
+ }
51116
+ }
51117
+ return headers;
51118
+ }
51119
51119
  function cookieValue(header, name) {
51120
51120
  if (!header) return null;
51121
51121
  for (const part of header.split(";")) {
@@ -93652,6 +93652,20 @@ function browserIdentityRouteError(error) {
93652
93652
  });
93653
93653
  }
93654
93654
 
93655
+ // src/attached-browser-viewer.ts
93656
+ import { requireLiveAgentAttemptAuthorization as requireLiveAgentAttemptAuthorization6 } from "@opengeni/core";
93657
+ async function attachedBrowserViewer(deps, grant, workspaceId) {
93658
+ let subjectId = grant.subjectId;
93659
+ if (grant.principalKind === "agent_attempt") {
93660
+ const callerSessionId = grant.metadata?.["sessionId"];
93661
+ if (typeof callerSessionId === "string") {
93662
+ const actor = await requireLiveAgentAttemptAuthorization6(deps.db, grant, callerSessionId);
93663
+ subjectId = actor.initiatingHumanSubjectId ?? grant.subjectId;
93664
+ }
93665
+ }
93666
+ return { accountId: grant.accountId, workspaceId, subjectId };
93667
+ }
93668
+
93655
93669
  // src/browser-ephemeral.ts
93656
93670
  import { createHmac as createHmac10 } from "crypto";
93657
93671
  import { BrowserControlUnsupportedError } from "@opengeni/runtime/sandbox";
@@ -94751,8 +94765,7 @@ function registerBrowserSessionRoutes(app, deps, options = {}) {
94751
94765
  return context.json(
94752
94766
  AttachedBrowserDeviceListResponse.parse(
94753
94767
  await listAttachedBrowserDevices(deps.db, {
94754
- accountId: grant.accountId,
94755
- workspaceId,
94768
+ ...await attachedBrowserViewer(deps, grant, workspaceId),
94756
94769
  includeDisconnected
94757
94770
  })
94758
94771
  )
@@ -94766,8 +94779,7 @@ function registerBrowserSessionRoutes(app, deps, options = {}) {
94766
94779
  return context.json(
94767
94780
  AttachedBrowserDevice.parse(
94768
94781
  await getAttachedBrowserDevice(deps.db, {
94769
- accountId: grant.accountId,
94770
- workspaceId,
94782
+ ...await attachedBrowserViewer(deps, grant, workspaceId),
94771
94783
  deviceId
94772
94784
  })
94773
94785
  )
@@ -97010,20 +97022,16 @@ function registerBrowserSessionRoutes(app, deps, options = {}) {
97010
97022
  async function withBrowserPlacement(sourceSession, grant, expectedPlacement, expectedPlacementInstanceId, operation, waitSignal, callback, allowOperationReplay = true) {
97011
97023
  if (expectedPlacement?.kind === "attached_device") {
97012
97024
  waitSignal.throwIfAborted();
97025
+ const viewer = await attachedBrowserViewer(deps, grant, sourceSession.workspaceId);
97013
97026
  const device = await getAttachedBrowserDevice(deps.db, {
97014
- accountId: grant.accountId,
97015
- workspaceId: sourceSession.workspaceId,
97027
+ ...viewer,
97016
97028
  deviceId: expectedPlacement.deviceId
97017
97029
  });
97018
97030
  if (device.state !== "connected") {
97019
97031
  recordAttachedBrowserUnavailable(deps.observability, "disconnected");
97020
97032
  throw new BrowserSessionStateError2("Attached browser is disconnected");
97021
97033
  }
97022
- const enrollment = await getLiveEnrollmentConnection6(
97023
- deps.db,
97024
- sourceSession.workspaceId,
97025
- device.enrollmentId
97026
- );
97034
+ const enrollment = await getLiveEnrollmentConnection6(deps.db, viewer, device.enrollmentId);
97027
97035
  if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
97028
97036
  recordAttachedBrowserUnavailable(deps.observability, "machine_unavailable");
97029
97037
  throw new BrowserSessionStateError2("Attached browser machine is unavailable");
@@ -99744,19 +99752,15 @@ function registerComputerSessionRoutes(app, deps, options = {}) {
99744
99752
  }
99745
99753
  if (expectedPlacement?.kind === "attached_device") {
99746
99754
  waitSignal.throwIfAborted();
99755
+ const viewer = await attachedBrowserViewer(deps, grant, sourceSession.workspaceId);
99747
99756
  const device = await getAttachedBrowserDevice2(deps.db, {
99748
- accountId: grant.accountId,
99749
- workspaceId: sourceSession.workspaceId,
99757
+ ...viewer,
99750
99758
  deviceId: expectedPlacement.deviceId
99751
99759
  });
99752
99760
  if (device.state !== "connected") {
99753
99761
  throw new ComputerSessionStateError("Attached browser machine is disconnected");
99754
99762
  }
99755
- const enrollment = await getLiveEnrollmentConnection7(
99756
- deps.db,
99757
- sourceSession.workspaceId,
99758
- device.enrollmentId
99759
- );
99763
+ const enrollment = await getLiveEnrollmentConnection7(deps.db, viewer, device.enrollmentId);
99760
99764
  if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
99761
99765
  throw new ComputerSessionStateError("Attached browser machine is unavailable");
99762
99766
  }
@@ -115514,6 +115518,12 @@ function sessionAuthorizationHttpError(error) {
115514
115518
  if (error instanceof HTTPException99) return error;
115515
115519
  throw error;
115516
115520
  }
115521
+ var SESSION_FORK_POINT_REFUSAL_MESSAGES = {
115522
+ message_unavailable: "Only your messages and the agent's replies can be fork points. Choose one of those, or fork the whole session.",
115523
+ boundary_not_retained: "This message is from before the conversation was compacted, so it can't be a fork point. Choose a more recent message, or fork the whole session.",
115524
+ history_compacted: "The conversation up to this message was compacted, so it can't be copied as it was then. Choose a more recent message, or fork the whole session.",
115525
+ unfinished_tool_exchange: "The agent was still waiting for a tool result at this message. Choose a later message, or fork the whole session."
115526
+ };
115517
115527
  function sessionTenancyHttpError(error) {
115518
115528
  if (error instanceof SessionTenancyManagedHumanRequiredError2) {
115519
115529
  return new ApiHttpError(403, {
@@ -115565,6 +115575,14 @@ function sessionTenancyHttpError(error) {
115565
115575
  });
115566
115576
  }
115567
115577
  if (error instanceof SessionTenancyInvalidRequestError) {
115578
+ if (error.forkPointRefusal) {
115579
+ return new ApiHttpError(422, {
115580
+ code: "validation_failed",
115581
+ message: SESSION_FORK_POINT_REFUSAL_MESSAGES[error.forkPointRefusal],
115582
+ retryable: false,
115583
+ details: { reason: `fork_point_${error.forkPointRefusal}` }
115584
+ });
115585
+ }
115568
115586
  return new ApiHttpError(422, {
115569
115587
  code: "validation_failed",
115570
115588
  message: "Invalid session tenancy request.",
@@ -121038,7 +121056,7 @@ import {
121038
121056
  grantHasAgentAttemptAuthority as grantHasAgentAttemptAuthority4,
121039
121057
  hasPermission as hasPermission34,
121040
121058
  requireAccessGrantAuthorization as requireAccessGrantAuthorization37,
121041
- requireLiveAgentAttemptAuthorization as requireLiveAgentAttemptAuthorization6,
121059
+ requireLiveAgentAttemptAuthorization as requireLiveAgentAttemptAuthorization7,
121042
121060
  requireSessionAuthorization as requireSessionAuthorization18,
121043
121061
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError17,
121044
121062
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError15,
@@ -121228,7 +121246,7 @@ function registerArtifactCatalogRoutes(app, deps) {
121228
121246
  try {
121229
121247
  if (typeof callerSessionId !== "string")
121230
121248
  throw new SessionAuthorizationDeniedError17("caller_stale");
121231
- const attempt = await requireLiveAgentAttemptAuthorization6(deps.db, grant, callerSessionId);
121249
+ const attempt = await requireLiveAgentAttemptAuthorization7(deps.db, grant, callerSessionId);
121232
121250
  actor.initiatingHumanSubjectId = attempt.initiatingHumanSubjectId;
121233
121251
  } catch (error) {
121234
121252
  if (error instanceof SessionAuthorizationDeniedError17)
@@ -127743,12 +127761,23 @@ var CODE_IDENTIFIER_PREFIX = "opengeni-native-app-code:";
127743
127761
  var PKCE_VERIFIER2 = /^[A-Za-z0-9._~-]{43,128}$/u;
127744
127762
  var PKCE_CHALLENGE2 = /^[A-Za-z0-9_-]{43}$/u;
127745
127763
  var MANAGED_PROVIDERS = ["credential", "google", "github"];
127764
+ var NATIVE_APP_PLATFORMS = ["ios", "android", "macos", "windows", "linux"];
127765
+ var PLATFORM_LABELS = {
127766
+ ios: "iOS",
127767
+ android: "Android",
127768
+ macos: "macOS",
127769
+ windows: "Windows",
127770
+ linux: "Linux"
127771
+ };
127772
+ function nativeAppSessionUserAgent(platform, deviceName) {
127773
+ return `Opengeni app (${PLATFORM_LABELS[platform]}${deviceName ? `; ${deviceName}` : ""})`;
127774
+ }
127746
127775
  var AuthorizeBody = z71.object({
127747
127776
  redirectUri: z71.string().min(1).max(512),
127748
127777
  codeChallenge: z71.string().regex(PKCE_CHALLENGE2),
127749
127778
  codeChallengeMethod: z71.literal("S256"),
127750
127779
  state: z71.string().min(8).max(256).optional(),
127751
- platform: z71.enum(["ios", "android"]),
127780
+ platform: z71.enum(NATIVE_APP_PLATFORMS),
127752
127781
  deviceName: z71.string().trim().min(1).max(80).optional()
127753
127782
  });
127754
127783
  var TokenBody = z71.object({
@@ -127768,7 +127797,7 @@ var StoredGrant = z71.object({
127768
127797
  providerId: z71.enum(MANAGED_PROVIDERS),
127769
127798
  redirectUri: z71.string(),
127770
127799
  codeChallenge: z71.string().regex(PKCE_CHALLENGE2),
127771
- platform: z71.enum(["ios", "android"]),
127800
+ platform: z71.enum(NATIVE_APP_PLATFORMS),
127772
127801
  deviceName: z71.string().nullable()
127773
127802
  });
127774
127803
  function nativeAppRedirectAllowed(redirectUri, schemes) {
@@ -127867,11 +127896,10 @@ function registerNativeAppAuthRoutes(app, deps) {
127867
127896
  if (!sameText(grant.redirectUri, body4.redirectUri) || !sameText(grant.codeChallenge, pkceS256Challenge(body4.codeVerifier))) {
127868
127897
  return context.json({ error: "invalid_grant" }, 400);
127869
127898
  }
127870
- const platform = grant.platform === "ios" ? "iOS" : "Android";
127871
127899
  const session = await runManagedAuthProvider(
127872
127900
  grant.providerId,
127873
127901
  () => authContext.internalAdapter.createSession(grant.userId, false, {
127874
- userAgent: `Opengeni app (${platform}${grant.deviceName ? `; ${grant.deviceName}` : ""})`
127902
+ userAgent: nativeAppSessionUserAgent(grant.platform, grant.deviceName)
127875
127903
  })
127876
127904
  );
127877
127905
  if (!session?.token || new Date(session.expiresAt).getTime() <= Date.now()) {
@@ -127897,6 +127925,31 @@ function registerNativeAppAuthRoutes(app, deps) {
127897
127925
  }
127898
127926
  return context.json({ signedOut: true });
127899
127927
  });
127928
+ app.post(`${base}/web-session`, async (context) => {
127929
+ const auth = deps.managedAuth;
127930
+ if (!auth) throw new HTTPException126(404);
127931
+ if (deps.settings.managedAuthSessionSetMode === "broker") {
127932
+ throw new HTTPException126(409, {
127933
+ message: "This deployment signs browser windows in through login session sets"
127934
+ });
127935
+ }
127936
+ const credential = bearer(context.req.header("authorization"));
127937
+ if (!credential?.startsWith(NATIVE_APP_CREDENTIAL_PREFIX)) {
127938
+ throw new HTTPException126(401, { message: "Native app credential required" });
127939
+ }
127940
+ const session = await getNativeAppManagedSession(auth, credential, deps.db).catch(() => null);
127941
+ if (typeof session?.session?.id !== "string") {
127942
+ throw new HTTPException126(401, { message: "Sign in again" });
127943
+ }
127944
+ for (const cookie of await managedAuthSessionCookieHeaders(
127945
+ auth,
127946
+ credential.slice(NATIVE_APP_CREDENTIAL_PREFIX.length),
127947
+ context.req.header("cookie") ?? null
127948
+ )) {
127949
+ context.header("set-cookie", cookie, { append: true });
127950
+ }
127951
+ return context.json({ signedIn: true });
127952
+ });
127900
127953
  const appSessionId = async (authorization) => {
127901
127954
  const auth = deps.managedAuth;
127902
127955
  if (!auth) throw new HTTPException126(404);
@@ -135416,6 +135469,7 @@ function createAppComposition(deps) {
135416
135469
  ClientConfig.parse({
135417
135470
  deploymentRevision: deps.settings.deploymentRevision,
135418
135471
  claudeSubscriptionEnabled: deps.settings.claudeSubscriptionEnabled,
135472
+ connectedMachines: deps.settings.sandboxSelfhostedEnabled,
135419
135473
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION4,
135420
135474
  ...deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {},
135421
135475
  defaultModel,
@@ -137625,4 +137679,4 @@ export {
137625
137679
  withDefaultEnabledCapabilityMcpTools,
137626
137680
  workflowIdForSession4 as workflowIdForSession
137627
137681
  };
137628
- //# sourceMappingURL=chunk-D5WJ24GY.js.map
137682
+ //# sourceMappingURL=chunk-ZDTWXRFX.js.map