@opengeni/api-router 0.30.1 → 2.1.0-canary.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/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
- package/dist/chunk-QKDFBBUE.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-office-import.d.ts +7 -4
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +364 -86
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +19 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +149 -9
- package/dist/integrations/slack-interactions.d.ts +10 -4
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +17 -3
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +160 -27
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +77 -9
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +416 -101
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +337 -57
- package/src/integrations/slack-interactions.ts +1274 -214
- package/src/integrations/slack-routing.ts +324 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +22 -2
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +388 -98
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +145 -34
- package/src/routes/documents.ts +221 -3
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +152 -27
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +777 -40
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-2PQMSRCB.js.map +0 -1
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
type SiteAuthAuthority,
|
|
60
60
|
} from "@opengeni/contracts";
|
|
61
61
|
import {
|
|
62
|
+
getSessionAuthorityEpoch,
|
|
62
63
|
acquireLease,
|
|
63
64
|
activateBrowserSession,
|
|
64
65
|
ATTACHED_BROWSER_SESSION_CAPABILITIES,
|
|
@@ -95,12 +96,13 @@ import {
|
|
|
95
96
|
findBrowserDownloadSave,
|
|
96
97
|
getBrowserSessionControlRecord,
|
|
97
98
|
getComputerSessionControlRecord,
|
|
99
|
+
getBrowserIdentity,
|
|
98
100
|
getBrowserPrivateCheckpointAuthority,
|
|
99
101
|
getAttachedBrowserDevice,
|
|
100
102
|
getBrowserRevisionArtifactAuthority,
|
|
101
|
-
|
|
103
|
+
getLiveEnrollmentConnection,
|
|
102
104
|
getExternalAuthInteractiveContext,
|
|
103
|
-
|
|
105
|
+
getFilesForSubject,
|
|
104
106
|
getFileUpload,
|
|
105
107
|
getAuthRun,
|
|
106
108
|
getExternalAuthPreparation,
|
|
@@ -131,6 +133,7 @@ import {
|
|
|
131
133
|
touchBrowserSessionController,
|
|
132
134
|
verifyAuthRun,
|
|
133
135
|
settleBrowserDownloadSaveFailure,
|
|
136
|
+
terminalizeStaleConnectedInteractionPlacement,
|
|
134
137
|
type BrowserSessionControlRecord,
|
|
135
138
|
type BrowserPrivateCheckpointAuthority,
|
|
136
139
|
type BrowserRevisionArtifactAuthority,
|
|
@@ -142,9 +145,11 @@ import {
|
|
|
142
145
|
recordWorkspaceUsage,
|
|
143
146
|
requireLimit,
|
|
144
147
|
relayConfigFromSettings,
|
|
148
|
+
resolveSessionSandboxRuntime,
|
|
145
149
|
SessionAuthorizationDeniedError,
|
|
146
150
|
SessionAuthorizationUnavailableError,
|
|
147
151
|
type ApiRouteDeps,
|
|
152
|
+
type ResolvedSessionAuthorization,
|
|
148
153
|
} from "@opengeni/core";
|
|
149
154
|
import {
|
|
150
155
|
BrowserControlProtocolError,
|
|
@@ -157,16 +162,19 @@ import {
|
|
|
157
162
|
buildStreamUrl,
|
|
158
163
|
exposedPortEndpointFromUrl,
|
|
159
164
|
buildSelfhostedBackendSession,
|
|
165
|
+
ensureDisplayStack,
|
|
160
166
|
mintStreamToken,
|
|
161
167
|
NatsControlRpc,
|
|
162
168
|
NatsOpStreamTransport,
|
|
163
169
|
provisionBrowserControlClient,
|
|
170
|
+
renewSandboxProviderExpiration,
|
|
164
171
|
type BrowserControlPlacementSession,
|
|
165
172
|
type PlacementBrowserStateCaptureReceipt,
|
|
166
173
|
type PlacementBrowserNetworkRoute,
|
|
167
174
|
type PlacementBrowserTransport,
|
|
168
175
|
type RestorePlacementBrowserStateInput,
|
|
169
176
|
} from "@opengeni/runtime/sandbox";
|
|
177
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
170
178
|
import type { Context, Hono } from "hono";
|
|
171
179
|
import { HTTPException } from "hono/http-exception";
|
|
172
180
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
@@ -175,8 +183,14 @@ import {
|
|
|
175
183
|
deriveBrowserNetworkRouteAuthorityDigest,
|
|
176
184
|
deriveBrowserSessionControllerTokens,
|
|
177
185
|
deriveBrowserViewGrantToken,
|
|
186
|
+
deriveComputerSessionControllerTokens,
|
|
178
187
|
} from "../browser-controller-authority";
|
|
179
|
-
import {
|
|
188
|
+
import {
|
|
189
|
+
controllerCacheAllowsHostFetch,
|
|
190
|
+
controllerCachedUrlIsUsable,
|
|
191
|
+
shouldPersistControllerDataPlaneUrl,
|
|
192
|
+
withCachedController,
|
|
193
|
+
} from "../controller-data-plane";
|
|
180
194
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
181
195
|
import {
|
|
182
196
|
browserStateArtifactAad,
|
|
@@ -191,7 +205,13 @@ import {
|
|
|
191
205
|
resolveProtectedAuthFieldValues,
|
|
192
206
|
} from "../browser-auth-broker";
|
|
193
207
|
import { managedNetworkRouteForPlacement } from "../browser-network-route";
|
|
194
|
-
import {
|
|
208
|
+
import { validateInteractionRequestOrigin } from "../http/cors";
|
|
209
|
+
import { ApiHttpError } from "../http/api-error";
|
|
210
|
+
import { interactionControlApiError } from "../http/interaction-control-error";
|
|
211
|
+
import {
|
|
212
|
+
createInteractionFrameProxyAttachment,
|
|
213
|
+
placementUsesInteractionFrameProxy,
|
|
214
|
+
} from "../interaction-frame-proxy";
|
|
195
215
|
import {
|
|
196
216
|
observeAuthMutation,
|
|
197
217
|
observeBrowserActionResult,
|
|
@@ -297,7 +317,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
297
317
|
const request = await parseJsonBody(context, CreateBrowserSessionRequest);
|
|
298
318
|
const startedAtMs = performance.now();
|
|
299
319
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
300
|
-
const origin = requestOrigin(context, deps.settings
|
|
320
|
+
const origin = requestOrigin(context, deps.settings);
|
|
301
321
|
const authority = browserAuthorityRoot(deps);
|
|
302
322
|
|
|
303
323
|
try {
|
|
@@ -324,6 +344,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
324
344
|
return context.json(parsed, 200);
|
|
325
345
|
}
|
|
326
346
|
|
|
347
|
+
// Reject an unavailable explicit identity before acquiring or waking the
|
|
348
|
+
// browser placement. prepareBrowserSessionCreate repeats this check in its
|
|
349
|
+
// transaction, so an archive racing this preflight is still fenced.
|
|
350
|
+
if (!existing && request.identityId) {
|
|
351
|
+
const identity = await getBrowserIdentity(deps.db, {
|
|
352
|
+
accountId: grant.accountId,
|
|
353
|
+
workspaceId,
|
|
354
|
+
identityId: request.identityId,
|
|
355
|
+
});
|
|
356
|
+
if (identity.status !== "active") {
|
|
357
|
+
throw new BrowserIdentityStateError("BrowserIdentity is archived");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
327
361
|
const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
|
|
328
362
|
const response = await withBrowserPlacement(
|
|
329
363
|
sourceSession,
|
|
@@ -400,12 +434,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
400
434
|
});
|
|
401
435
|
const preparedSession = prepared.session;
|
|
402
436
|
const controllerGeneration = requireOperationGeneration(record);
|
|
403
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
404
|
-
deps,
|
|
405
|
-
grant,
|
|
406
|
-
prepared.session,
|
|
407
|
-
placement,
|
|
408
|
-
);
|
|
409
437
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
410
438
|
rootSecret: authority,
|
|
411
439
|
accountId: grant.accountId,
|
|
@@ -431,8 +459,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
431
459
|
placement,
|
|
432
460
|
adminToken,
|
|
433
461
|
origin,
|
|
434
|
-
async (client) =>
|
|
435
|
-
await
|
|
462
|
+
async (client) => {
|
|
463
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
464
|
+
deps,
|
|
465
|
+
grant,
|
|
466
|
+
preparedSession,
|
|
467
|
+
placement,
|
|
468
|
+
client,
|
|
469
|
+
);
|
|
470
|
+
return await withInteractionHolderHeartbeat(
|
|
436
471
|
deps,
|
|
437
472
|
{
|
|
438
473
|
grant,
|
|
@@ -443,8 +478,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
443
478
|
resourceId: preparedSession.id,
|
|
444
479
|
controllerGeneration,
|
|
445
480
|
},
|
|
446
|
-
async () =>
|
|
447
|
-
await
|
|
481
|
+
async () => {
|
|
482
|
+
await ensureHeadedBrowserDisplayStack(
|
|
483
|
+
placement.session,
|
|
484
|
+
preparedSession.headless,
|
|
485
|
+
linkedComputer !== null,
|
|
486
|
+
);
|
|
487
|
+
return await client.createSession({
|
|
448
488
|
browserSessionId: preparedSession.id,
|
|
449
489
|
controllerGeneration,
|
|
450
490
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -455,8 +495,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
455
495
|
...(networkRoute ? { networkRoute } : {}),
|
|
456
496
|
...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
|
|
457
497
|
...(restore ? { restore } : {}),
|
|
458
|
-
})
|
|
459
|
-
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
},
|
|
460
502
|
);
|
|
461
503
|
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
462
504
|
() => placement,
|
|
@@ -927,7 +969,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
927
969
|
browserSessionId,
|
|
928
970
|
"session.control",
|
|
929
971
|
"browser.action",
|
|
930
|
-
async ({ sessionClient, binding, record }) => {
|
|
972
|
+
async ({ sessionClient, binding, record, sourceAuthorization }) => {
|
|
931
973
|
const command = BrowserActionCommand.parse({
|
|
932
974
|
protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
933
975
|
operationId: request.operationId,
|
|
@@ -957,27 +999,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
957
999
|
message: "object storage is not configured",
|
|
958
1000
|
});
|
|
959
1001
|
}
|
|
960
|
-
const files = await
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
throw new HTTPException(409, { message: "workspace file is not ready" });
|
|
968
|
-
}
|
|
1002
|
+
const files = await getFilesForSubject(deps.db, {
|
|
1003
|
+
accountId: grant.accountId,
|
|
1004
|
+
workspaceId,
|
|
1005
|
+
subjectId: browserFileAuthoritySubjectId(grant, sourceAuthorization),
|
|
1006
|
+
fileIds: workspaceFileIds,
|
|
1007
|
+
});
|
|
1008
|
+
const ordered = requireAuthorizedBrowserUploadFiles(workspaceFileIds, files);
|
|
969
1009
|
const authorities = await Promise.all(
|
|
970
1010
|
ordered.map(async (file) => {
|
|
971
1011
|
const signed = await deps.objectStorage!.createGetUrl({
|
|
972
|
-
key: file
|
|
1012
|
+
key: file.objectKey,
|
|
973
1013
|
expiresInSeconds: BROWSER_WORKSPACE_FILE_AUTHORITY_TTL_SECONDS,
|
|
974
1014
|
audience: signedUrlAudienceForPlacement(record.session.placement),
|
|
975
1015
|
});
|
|
976
1016
|
return {
|
|
977
|
-
fileId: file
|
|
978
|
-
safeFilename: browserUploadFilename(file
|
|
979
|
-
sizeBytes: file
|
|
980
|
-
sha256: browserUploadSha256(file
|
|
1017
|
+
fileId: file.id,
|
|
1018
|
+
safeFilename: browserUploadFilename(file.safeFilename),
|
|
1019
|
+
sizeBytes: file.sizeBytes,
|
|
1020
|
+
sha256: browserUploadSha256(file.sha256),
|
|
981
1021
|
download: {
|
|
982
1022
|
url: signed.url,
|
|
983
1023
|
expiresAt: signed.expiresAt.toISOString(),
|
|
@@ -1664,7 +1704,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1664
1704
|
context,
|
|
1665
1705
|
"stream:view",
|
|
1666
1706
|
);
|
|
1667
|
-
const origin = requestOrigin(context, deps.settings
|
|
1707
|
+
const origin = requestOrigin(context, deps.settings);
|
|
1668
1708
|
const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
|
|
1669
1709
|
const result = await withActiveBrowserController(
|
|
1670
1710
|
context,
|
|
@@ -1720,6 +1760,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1720
1760
|
});
|
|
1721
1761
|
const stream = relayed
|
|
1722
1762
|
? await (async () => {
|
|
1763
|
+
// 0281: stamp the authenticated viewer subject and the live
|
|
1764
|
+
// session authority epoch into the relay stream token.
|
|
1765
|
+
const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
|
|
1766
|
+
accountId: grant.accountId,
|
|
1767
|
+
workspaceId,
|
|
1768
|
+
sessionId: record.sourceSessionId,
|
|
1769
|
+
});
|
|
1770
|
+
if (relayAuthorityEpoch === null) {
|
|
1771
|
+
throw new BrowserControlUnsupportedError(
|
|
1772
|
+
"stream authority is unavailable for this session",
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1723
1775
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
1724
1776
|
workspaceId,
|
|
1725
1777
|
sessionId: record.sourceSessionId,
|
|
@@ -1727,6 +1779,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1727
1779
|
leaseEpoch: record.tokenGeneration,
|
|
1728
1780
|
port: relayed.channel.port,
|
|
1729
1781
|
ttlSeconds: request.expiresInSeconds,
|
|
1782
|
+
subjectId: grant.subjectId,
|
|
1783
|
+
authorityEpoch: relayAuthorityEpoch,
|
|
1730
1784
|
});
|
|
1731
1785
|
return {
|
|
1732
1786
|
kind: "relay" as const,
|
|
@@ -1741,14 +1795,44 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1741
1795
|
},
|
|
1742
1796
|
};
|
|
1743
1797
|
})()
|
|
1744
|
-
: {
|
|
1745
|
-
|
|
1746
|
-
url: await client.frameStreamUrl(reference, request.targetId, request.stream),
|
|
1747
|
-
protocols: [
|
|
1798
|
+
: await (async () => {
|
|
1799
|
+
const protocols = [
|
|
1748
1800
|
BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
1749
1801
|
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
1750
|
-
]
|
|
1751
|
-
|
|
1802
|
+
] as const;
|
|
1803
|
+
const upstreamUrl = await client.frameStreamUrl(
|
|
1804
|
+
reference,
|
|
1805
|
+
request.targetId,
|
|
1806
|
+
request.stream,
|
|
1807
|
+
);
|
|
1808
|
+
const attachment = placementUsesInteractionFrameProxy(placement.lease?.backend, {
|
|
1809
|
+
openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
1810
|
+
...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
|
|
1811
|
+
? {
|
|
1812
|
+
openSandboxInteractionFrameProxy:
|
|
1813
|
+
deps.settings.openSandboxInteractionFrameProxy,
|
|
1814
|
+
}
|
|
1815
|
+
: {}),
|
|
1816
|
+
})
|
|
1817
|
+
? createInteractionFrameProxyAttachment({
|
|
1818
|
+
requestUrl: context.req.url,
|
|
1819
|
+
publicBaseUrl: deps.settings.publicBaseUrl,
|
|
1820
|
+
webBaseUrl: deps.settings.webBaseUrl,
|
|
1821
|
+
forwardedProto: context.req.header("x-forwarded-proto"),
|
|
1822
|
+
forwardedHost:
|
|
1823
|
+
context.req.header("x-forwarded-host") ?? context.req.header("host"),
|
|
1824
|
+
rootSecret,
|
|
1825
|
+
upstreamUrl,
|
|
1826
|
+
upstreamProtocols: protocols,
|
|
1827
|
+
origin,
|
|
1828
|
+
expiresAt,
|
|
1829
|
+
})
|
|
1830
|
+
: { url: upstreamUrl, protocols };
|
|
1831
|
+
return {
|
|
1832
|
+
kind: "direct_websocket" as const,
|
|
1833
|
+
...attachment,
|
|
1834
|
+
};
|
|
1835
|
+
})();
|
|
1752
1836
|
return BrowserSessionAttachment.parse({
|
|
1753
1837
|
browserSessionId,
|
|
1754
1838
|
controllerGeneration: binding.controllerGeneration,
|
|
@@ -1959,7 +2043,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1959
2043
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
1960
2044
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
1961
2045
|
const startedAtMs = performance.now();
|
|
1962
|
-
const origin = requestOrigin(context, deps.settings
|
|
2046
|
+
const origin = requestOrigin(context, deps.settings);
|
|
1963
2047
|
try {
|
|
1964
2048
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
1965
2049
|
accountId: grant.accountId,
|
|
@@ -2185,7 +2269,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2185
2269
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2186
2270
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2187
2271
|
const startedAtMs = performance.now();
|
|
2188
|
-
const origin = requestOrigin(context, deps.settings
|
|
2272
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2189
2273
|
let restore: RestorePlacementBrowserStateInput | null = null;
|
|
2190
2274
|
try {
|
|
2191
2275
|
let before = await getBrowserSessionControlRecord(deps.db, {
|
|
@@ -2304,12 +2388,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2304
2388
|
throw error;
|
|
2305
2389
|
});
|
|
2306
2390
|
const controllerGeneration = requireOperationGeneration(record);
|
|
2307
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
2308
|
-
deps,
|
|
2309
|
-
grant,
|
|
2310
|
-
prepared.session,
|
|
2311
|
-
placement,
|
|
2312
|
-
);
|
|
2313
2391
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
2314
2392
|
rootSecret: authority,
|
|
2315
2393
|
accountId: grant.accountId,
|
|
@@ -2333,6 +2411,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2333
2411
|
...(origin ? { allowedOrigins: [origin] } : {}),
|
|
2334
2412
|
});
|
|
2335
2413
|
try {
|
|
2414
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
2415
|
+
deps,
|
|
2416
|
+
grant,
|
|
2417
|
+
prepared.session,
|
|
2418
|
+
placement,
|
|
2419
|
+
client,
|
|
2420
|
+
);
|
|
2336
2421
|
await withInteractionHolderHeartbeat(
|
|
2337
2422
|
deps,
|
|
2338
2423
|
{
|
|
@@ -2344,8 +2429,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2344
2429
|
resourceId: browserSessionId,
|
|
2345
2430
|
controllerGeneration,
|
|
2346
2431
|
},
|
|
2347
|
-
async () =>
|
|
2348
|
-
await
|
|
2432
|
+
async () => {
|
|
2433
|
+
await ensureHeadedBrowserDisplayStack(
|
|
2434
|
+
placement.session,
|
|
2435
|
+
prepared.session.headless,
|
|
2436
|
+
linkedComputer !== null,
|
|
2437
|
+
);
|
|
2438
|
+
return await client.createSession({
|
|
2349
2439
|
browserSessionId,
|
|
2350
2440
|
controllerGeneration,
|
|
2351
2441
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -2355,7 +2445,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2355
2445
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
2356
2446
|
...(networkRoute ? { networkRoute } : {}),
|
|
2357
2447
|
restore: restore!,
|
|
2358
|
-
})
|
|
2448
|
+
});
|
|
2449
|
+
},
|
|
2359
2450
|
);
|
|
2360
2451
|
} catch (error) {
|
|
2361
2452
|
if (!isDefiniteBrowserControllerFailure(error)) throw error;
|
|
@@ -2427,7 +2518,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2427
2518
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2428
2519
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2429
2520
|
const startedAtMs = performance.now();
|
|
2430
|
-
const origin = requestOrigin(context, deps.settings
|
|
2521
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2431
2522
|
try {
|
|
2432
2523
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
2433
2524
|
accountId: grant.accountId,
|
|
@@ -2574,18 +2665,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2574
2665
|
if (device.state !== "connected") {
|
|
2575
2666
|
throw new BrowserSessionStateError("Attached browser is disconnected");
|
|
2576
2667
|
}
|
|
2577
|
-
const enrollment = await
|
|
2668
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
2578
2669
|
deps.db,
|
|
2579
2670
|
sourceSession.workspaceId,
|
|
2580
2671
|
device.enrollmentId,
|
|
2581
2672
|
);
|
|
2582
|
-
if (!enrollment || enrollment.status !== "active") {
|
|
2673
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
2583
2674
|
throw new BrowserSessionStateError("Attached browser machine is unavailable");
|
|
2584
2675
|
}
|
|
2585
|
-
|
|
2676
|
+
if (!enrollment.workspaceRoot) {
|
|
2677
|
+
throw new BrowserSessionStateError(
|
|
2678
|
+
"Attached browser machine has not reported an absolute workspace root",
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
const placementInstanceId = attachedEndPlacementInstanceId(
|
|
2682
|
+
operation,
|
|
2683
|
+
expectedPlacementInstanceId,
|
|
2684
|
+
device.connectionGeneration,
|
|
2685
|
+
);
|
|
2586
2686
|
const built = await buildSelfhostedBackendSession({
|
|
2587
2687
|
workspaceId: sourceSession.workspaceId,
|
|
2588
2688
|
agentId: device.enrollmentId,
|
|
2689
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
2690
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
2589
2691
|
relay: relayConfigFromSettings(deps.settings),
|
|
2590
2692
|
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
2591
2693
|
// Browser-profile generation is the physical controller fence. Epoch 0
|
|
@@ -2594,6 +2696,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2594
2696
|
epoch: 0,
|
|
2595
2697
|
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
2596
2698
|
execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
|
|
2699
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
2700
|
+
operationResourcePolicySupported:
|
|
2701
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
2702
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
2597
2703
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
2598
2704
|
enrollment.opStream === true &&
|
|
2599
2705
|
deps.bus.getOpStreamConnection
|
|
@@ -2610,13 +2716,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2610
2716
|
return await callback({
|
|
2611
2717
|
placement: expectedPlacement,
|
|
2612
2718
|
controllerHostSandboxGroupId: null,
|
|
2613
|
-
placementInstanceId
|
|
2719
|
+
placementInstanceId,
|
|
2614
2720
|
session: built.session as unknown as BrowserControlPlacementSession,
|
|
2615
2721
|
lease: null,
|
|
2616
2722
|
transport: {
|
|
2617
2723
|
kind: "attached_chrome",
|
|
2618
2724
|
deviceId: device.id,
|
|
2619
|
-
connectionGeneration:
|
|
2725
|
+
connectionGeneration: placementInstanceId,
|
|
2620
2726
|
browserName: device.browserName,
|
|
2621
2727
|
browserVersion: device.browserVersion,
|
|
2622
2728
|
},
|
|
@@ -2682,8 +2788,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2682
2788
|
resolved.kind !== "selfhosted" ||
|
|
2683
2789
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
2684
2790
|
) {
|
|
2685
|
-
|
|
2686
|
-
|
|
2791
|
+
return await throwBrowserSourcePlacementChanged(
|
|
2792
|
+
grant,
|
|
2793
|
+
sourceSession.id,
|
|
2794
|
+
expectedPlacement.sandboxId,
|
|
2687
2795
|
);
|
|
2688
2796
|
}
|
|
2689
2797
|
const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
|
|
@@ -2780,7 +2888,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2780
2888
|
!lease ||
|
|
2781
2889
|
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
2782
2890
|
lease.instanceId !== binding.placementInstanceId ||
|
|
2783
|
-
!lease.controllerDataPlaneUrl
|
|
2891
|
+
!lease.controllerDataPlaneUrl ||
|
|
2892
|
+
(lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
|
|
2893
|
+
!controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
|
|
2784
2894
|
) {
|
|
2785
2895
|
return null;
|
|
2786
2896
|
}
|
|
@@ -2806,8 +2916,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2806
2916
|
if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
|
|
2807
2917
|
return placement;
|
|
2808
2918
|
}
|
|
2919
|
+
if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
|
|
2920
|
+
if (!placement.lease.controllerDataPlaneUrl) return placement;
|
|
2921
|
+
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2922
|
+
accountId: grant.accountId,
|
|
2923
|
+
workspaceId,
|
|
2924
|
+
sandboxGroupId,
|
|
2925
|
+
expectedEpoch: placement.lease.leaseEpoch,
|
|
2926
|
+
expectedInstanceId: placement.lease.instanceId,
|
|
2927
|
+
controllerDataPlaneUrl: null,
|
|
2928
|
+
});
|
|
2929
|
+
return lease ? { ...placement, lease } : placement;
|
|
2930
|
+
}
|
|
2809
2931
|
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
2810
2932
|
const url = buildStreamUrl(endpoint);
|
|
2933
|
+
if (
|
|
2934
|
+
!shouldPersistControllerDataPlaneUrl({
|
|
2935
|
+
backend: placement.lease.backend,
|
|
2936
|
+
signedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
2937
|
+
url,
|
|
2938
|
+
})
|
|
2939
|
+
) {
|
|
2940
|
+
return placement;
|
|
2941
|
+
}
|
|
2811
2942
|
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2812
2943
|
accountId: grant.accountId,
|
|
2813
2944
|
workspaceId,
|
|
@@ -2817,6 +2948,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2817
2948
|
controllerDataPlaneUrl: url,
|
|
2818
2949
|
});
|
|
2819
2950
|
if (!lease) return placement;
|
|
2951
|
+
if (!controllerCacheAllowsHostFetch(url)) {
|
|
2952
|
+
return { ...placement, lease };
|
|
2953
|
+
}
|
|
2820
2954
|
return { ...placement, session: controllerOnlySession(url), lease };
|
|
2821
2955
|
}
|
|
2822
2956
|
|
|
@@ -2832,7 +2966,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2832
2966
|
const sandboxGroupId = placement.controllerHostSandboxGroupId;
|
|
2833
2967
|
const cachedUrl = placement.lease?.controllerDataPlaneUrl;
|
|
2834
2968
|
return await withCachedController({
|
|
2835
|
-
cachedUrl:
|
|
2969
|
+
cachedUrl:
|
|
2970
|
+
sandboxGroupId &&
|
|
2971
|
+
cachedUrl &&
|
|
2972
|
+
!(
|
|
2973
|
+
placement.lease?.backend === "opensandbox" &&
|
|
2974
|
+
routeDeps.settings.openSandboxSignedEndpoints
|
|
2975
|
+
) &&
|
|
2976
|
+
controllerCachedUrlIsUsable(cachedUrl)
|
|
2977
|
+
? cachedUrl
|
|
2978
|
+
: null,
|
|
2836
2979
|
createCachedClient: (url) =>
|
|
2837
2980
|
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
2838
2981
|
prepareCachedClient: async (client) => {
|
|
@@ -2874,6 +3017,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2874
3017
|
record: BrowserSessionControlRecord;
|
|
2875
3018
|
binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
|
|
2876
3019
|
placement: BrowserPlacement;
|
|
3020
|
+
sourceAuthorization: ResolvedSessionAuthorization | null;
|
|
2877
3021
|
}) => Promise<T>,
|
|
2878
3022
|
recoverMissing = true,
|
|
2879
3023
|
): Promise<T> {
|
|
@@ -2883,7 +3027,23 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2883
3027
|
workspaceId,
|
|
2884
3028
|
browserSessionId,
|
|
2885
3029
|
});
|
|
2886
|
-
await authorizeSourceSession(
|
|
3030
|
+
const sourceAuthorization = await authorizeSourceSession(
|
|
3031
|
+
deps,
|
|
3032
|
+
grant,
|
|
3033
|
+
record.sourceSessionId,
|
|
3034
|
+
authorizationOperation,
|
|
3035
|
+
);
|
|
3036
|
+
let sourceSession: Session | null = null;
|
|
3037
|
+
if (record.session.placement?.kind === "connected_machine") {
|
|
3038
|
+
sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
3039
|
+
if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
|
|
3040
|
+
return await throwBrowserSourcePlacementChanged(
|
|
3041
|
+
grant,
|
|
3042
|
+
record.sourceSessionId,
|
|
3043
|
+
record.session.placement.sandboxId,
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
2887
3047
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
2888
3048
|
throw new BrowserSessionStateError("BrowserSession is not active");
|
|
2889
3049
|
}
|
|
@@ -2923,6 +3083,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2923
3083
|
record,
|
|
2924
3084
|
binding,
|
|
2925
3085
|
placement,
|
|
3086
|
+
sourceAuthorization,
|
|
2926
3087
|
};
|
|
2927
3088
|
let result: T;
|
|
2928
3089
|
try {
|
|
@@ -2967,7 +3128,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2967
3128
|
}
|
|
2968
3129
|
}
|
|
2969
3130
|
|
|
2970
|
-
|
|
3131
|
+
sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
2971
3132
|
return await withBrowserPlacement(
|
|
2972
3133
|
sourceSession,
|
|
2973
3134
|
grant,
|
|
@@ -2987,6 +3148,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2987
3148
|
}
|
|
2988
3149
|
}
|
|
2989
3150
|
|
|
3151
|
+
async function throwBrowserSourcePlacementChanged(
|
|
3152
|
+
grant: AccessGrant,
|
|
3153
|
+
sourceSessionId: string,
|
|
3154
|
+
connectedSandboxId: string,
|
|
3155
|
+
): Promise<never> {
|
|
3156
|
+
const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
|
|
3157
|
+
accountId: grant.accountId,
|
|
3158
|
+
workspaceId: grant.workspaceId,
|
|
3159
|
+
sourceSessionId,
|
|
3160
|
+
connectedSandboxId,
|
|
3161
|
+
});
|
|
3162
|
+
if (reconciliation.sourcePlacementChanged) {
|
|
3163
|
+
throw sourcePlacementChangedApiError("browser_session");
|
|
3164
|
+
}
|
|
3165
|
+
throw new BrowserSessionStateError(
|
|
3166
|
+
"BrowserSession source placement changed during the request; retry",
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
|
|
2990
3170
|
async function recoverActiveBrowserController(
|
|
2991
3171
|
routeDeps: ApiRouteDeps,
|
|
2992
3172
|
grant: AccessGrant,
|
|
@@ -2996,11 +3176,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2996
3176
|
client: BrowserControlClient,
|
|
2997
3177
|
tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
|
|
2998
3178
|
): Promise<void> {
|
|
2999
|
-
const linkedComputer = await
|
|
3179
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
3000
3180
|
routeDeps,
|
|
3001
3181
|
grant,
|
|
3002
3182
|
record.session,
|
|
3003
3183
|
placement,
|
|
3184
|
+
client,
|
|
3004
3185
|
);
|
|
3005
3186
|
const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
|
|
3006
3187
|
deps: routeDeps,
|
|
@@ -3009,6 +3190,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3009
3190
|
placement: placement.placement,
|
|
3010
3191
|
});
|
|
3011
3192
|
try {
|
|
3193
|
+
await ensureHeadedBrowserDisplayStack(
|
|
3194
|
+
placement.session,
|
|
3195
|
+
record.session.headless,
|
|
3196
|
+
linkedComputer !== null,
|
|
3197
|
+
);
|
|
3012
3198
|
await client.createSession({
|
|
3013
3199
|
browserSessionId: record.session.id,
|
|
3014
3200
|
controllerGeneration: binding.controllerGeneration,
|
|
@@ -3059,7 +3245,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3059
3245
|
grant,
|
|
3060
3246
|
record,
|
|
3061
3247
|
placement,
|
|
3062
|
-
requestOrigin(context, deps.settings
|
|
3248
|
+
requestOrigin(context, deps.settings),
|
|
3063
3249
|
);
|
|
3064
3250
|
await endCapturedBrowserController(client, browserSessionId, binding);
|
|
3065
3251
|
await clearSuspendedBrowserSessionController(deps.db, {
|
|
@@ -3081,6 +3267,21 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3081
3267
|
}
|
|
3082
3268
|
}
|
|
3083
3269
|
|
|
3270
|
+
function sourcePlacementChangedApiError(interactionResource: "browser_session"): ApiHttpError {
|
|
3271
|
+
return new ApiHttpError(409, {
|
|
3272
|
+
code: "conflict",
|
|
3273
|
+
message:
|
|
3274
|
+
"This browser belonged to a previous task placement and was retired. Open a new browser.",
|
|
3275
|
+
retryable: false,
|
|
3276
|
+
outcomeUnknown: false,
|
|
3277
|
+
details: {
|
|
3278
|
+
interactionResource,
|
|
3279
|
+
interactionFailureCode: "source_placement_changed",
|
|
3280
|
+
interactionLifecycle: "lost",
|
|
3281
|
+
},
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3084
3285
|
function browserCreateInput(
|
|
3085
3286
|
grant: AccessGrant,
|
|
3086
3287
|
workspaceId: string,
|
|
@@ -3229,11 +3430,31 @@ function assertCreateReplay(
|
|
|
3229
3430
|
}
|
|
3230
3431
|
}
|
|
3231
3432
|
|
|
3232
|
-
async function
|
|
3433
|
+
async function ensureHeadedBrowserDisplayStack(
|
|
3434
|
+
session: BrowserControlPlacementSession,
|
|
3435
|
+
headless: boolean,
|
|
3436
|
+
linkedComputer: boolean,
|
|
3437
|
+
): Promise<void> {
|
|
3438
|
+
if (!browserNeedsStandaloneDisplayStack({ headless, linkedComputer })) return;
|
|
3439
|
+
if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
|
|
3440
|
+
await ensureDisplayStack(session, {
|
|
3441
|
+
telemetryContext: { callerKind: "viewer" },
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
export function browserNeedsStandaloneDisplayStack(input: {
|
|
3446
|
+
headless: boolean;
|
|
3447
|
+
linkedComputer: boolean;
|
|
3448
|
+
}): boolean {
|
|
3449
|
+
return !input.headless && !input.linkedComputer;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
async function ensureLinkedComputerController(
|
|
3233
3453
|
deps: ApiRouteDeps,
|
|
3234
3454
|
grant: AccessGrant,
|
|
3235
3455
|
browser: BrowserSessionValue,
|
|
3236
3456
|
placement: BrowserPlacement,
|
|
3457
|
+
client: BrowserControlClient,
|
|
3237
3458
|
): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
|
|
3238
3459
|
if (!browser.linkedComputerSessionId) return null;
|
|
3239
3460
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -3251,10 +3472,42 @@ async function requireLinkedComputerBinding(
|
|
|
3251
3472
|
"Linked ComputerSession is not active on the browser placement",
|
|
3252
3473
|
);
|
|
3253
3474
|
}
|
|
3254
|
-
|
|
3475
|
+
const reference = {
|
|
3255
3476
|
computerSessionId: record.session.id,
|
|
3256
3477
|
controllerGeneration: record.session.controller.controllerGeneration,
|
|
3257
3478
|
};
|
|
3479
|
+
const tokens = deriveComputerSessionControllerTokens({
|
|
3480
|
+
rootSecret: browserAuthorityRoot(deps),
|
|
3481
|
+
accountId: grant.accountId,
|
|
3482
|
+
workspaceId: browser.workspaceId,
|
|
3483
|
+
placement: record.session.placement,
|
|
3484
|
+
placementInstanceId: placement.placementInstanceId,
|
|
3485
|
+
computerSessionId: record.session.id,
|
|
3486
|
+
controllerGeneration: record.session.controller.controllerGeneration,
|
|
3487
|
+
tokenGeneration: record.tokenGeneration,
|
|
3488
|
+
});
|
|
3489
|
+
const sessionClient = client.computerSessionClient({ reference, ...tokens });
|
|
3490
|
+
try {
|
|
3491
|
+
await sessionClient.heartbeat();
|
|
3492
|
+
} catch (error) {
|
|
3493
|
+
if (!isMissingLinkedComputerControllerSession(error)) throw error;
|
|
3494
|
+
await client.createComputerSession({
|
|
3495
|
+
...reference,
|
|
3496
|
+
tokenGeneration: record.tokenGeneration,
|
|
3497
|
+
...tokens,
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
return reference;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
function isMissingLinkedComputerControllerSession(error: unknown): boolean {
|
|
3504
|
+
if (!(error instanceof BrowserControlRequestError)) return false;
|
|
3505
|
+
if (error.status === 401 && error.error.code === "permission_denied") return true;
|
|
3506
|
+
return (
|
|
3507
|
+
error.status === 404 &&
|
|
3508
|
+
error.error.code === "resource_not_found" &&
|
|
3509
|
+
["computer session not found", "computer session is not active"].includes(error.error.message)
|
|
3510
|
+
);
|
|
3258
3511
|
}
|
|
3259
3512
|
|
|
3260
3513
|
async function requireBrowserRevisionRestoreAuthority(
|
|
@@ -3474,6 +3727,7 @@ async function ensureInteractionHolder(
|
|
|
3474
3727
|
if (!placement.lease?.instanceId) {
|
|
3475
3728
|
throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
|
|
3476
3729
|
}
|
|
3730
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
|
|
3477
3731
|
const acquired = await acquireLease(deps.db, {
|
|
3478
3732
|
accountId: grant.accountId,
|
|
3479
3733
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -3483,8 +3737,8 @@ async function ensureInteractionHolder(
|
|
|
3483
3737
|
subjectId: sourceSession.id,
|
|
3484
3738
|
backend: placement.lease.backend,
|
|
3485
3739
|
os: placement.lease.os,
|
|
3486
|
-
image:
|
|
3487
|
-
rigVersionId:
|
|
3740
|
+
image: sandboxRuntime.image,
|
|
3741
|
+
rigVersionId: sourceSession.rigVersionId,
|
|
3488
3742
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
3489
3743
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
3490
3744
|
waitSignal,
|
|
@@ -3506,6 +3760,11 @@ async function ensureInteractionHolder(
|
|
|
3506
3760
|
).catch(() => undefined);
|
|
3507
3761
|
throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
|
|
3508
3762
|
}
|
|
3763
|
+
await renewSandboxProviderExpiration({
|
|
3764
|
+
backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
|
|
3765
|
+
settings: deps.settings,
|
|
3766
|
+
instanceId: acquired.lease.instanceId,
|
|
3767
|
+
}).catch(() => false);
|
|
3509
3768
|
return true;
|
|
3510
3769
|
}
|
|
3511
3770
|
|
|
@@ -3792,9 +4051,9 @@ async function authorizeSourceSession(
|
|
|
3792
4051
|
grant: AccessGrant,
|
|
3793
4052
|
sessionId: string,
|
|
3794
4053
|
operation: SessionAuthorizationOperation,
|
|
3795
|
-
): Promise<
|
|
4054
|
+
): Promise<ResolvedSessionAuthorization | null> {
|
|
3796
4055
|
try {
|
|
3797
|
-
await requireSessionAuthorization(deps, grant, {
|
|
4056
|
+
return await requireSessionAuthorization(deps, grant, {
|
|
3798
4057
|
sessionId,
|
|
3799
4058
|
operation,
|
|
3800
4059
|
surface: "http",
|
|
@@ -3896,32 +4155,8 @@ function isUuid(value: unknown): value is string {
|
|
|
3896
4155
|
);
|
|
3897
4156
|
}
|
|
3898
4157
|
|
|
3899
|
-
function requestOrigin(context: Context,
|
|
3900
|
-
return
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
export function validateBrowserRequestOrigin(
|
|
3904
|
-
value: string | undefined,
|
|
3905
|
-
allowedPattern: string,
|
|
3906
|
-
): string | null {
|
|
3907
|
-
if (!value) return null;
|
|
3908
|
-
let url: URL;
|
|
3909
|
-
try {
|
|
3910
|
-
url = new URL(value);
|
|
3911
|
-
} catch {
|
|
3912
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3913
|
-
}
|
|
3914
|
-
if (
|
|
3915
|
-
url.origin === "null" ||
|
|
3916
|
-
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
3917
|
-
url.origin !== value
|
|
3918
|
-
) {
|
|
3919
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3920
|
-
}
|
|
3921
|
-
if (!allowedCorsOrigin(allowedPattern, url.origin)) {
|
|
3922
|
-
throw new HTTPException(403, { message: "request origin is not allowed" });
|
|
3923
|
-
}
|
|
3924
|
-
return url.origin;
|
|
4158
|
+
function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
|
|
4159
|
+
return validateInteractionRequestOrigin(context.req.header("origin"), settings);
|
|
3925
4160
|
}
|
|
3926
4161
|
|
|
3927
4162
|
export function interactionActorForGrant(
|
|
@@ -3949,6 +4184,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
|
|
|
3949
4184
|
}
|
|
3950
4185
|
}
|
|
3951
4186
|
|
|
4187
|
+
/** End keeps the original controller token fence so a later Chrome generation
|
|
4188
|
+
* can still dispose the stale browserd session. */
|
|
4189
|
+
function attachedEndPlacementInstanceId(
|
|
4190
|
+
operation: ChannelAOperation,
|
|
4191
|
+
expectedPlacementInstanceId: string | null,
|
|
4192
|
+
liveGeneration: string,
|
|
4193
|
+
): string {
|
|
4194
|
+
if (operation === "browser.end" && expectedPlacementInstanceId) {
|
|
4195
|
+
return expectedPlacementInstanceId;
|
|
4196
|
+
}
|
|
4197
|
+
assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
|
|
4198
|
+
return liveGeneration;
|
|
4199
|
+
}
|
|
4200
|
+
|
|
3952
4201
|
function isTerminalOperation(state: string): boolean {
|
|
3953
4202
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
3954
4203
|
}
|
|
@@ -4285,6 +4534,40 @@ function browserUploadFileIds(action: BrowserActionRequestValue["action"]): stri
|
|
|
4285
4534
|
];
|
|
4286
4535
|
}
|
|
4287
4536
|
|
|
4537
|
+
/** Resolve Drive authority from the same immutable session authorization that
|
|
4538
|
+
* admitted the browser action. Agent-attempt subjects are technical worker
|
|
4539
|
+
* identities; their frozen initiating human is the only personal Drive
|
|
4540
|
+
* principal. Pure service attempts retain null and can read only ordinary
|
|
4541
|
+
* workspace files through the database predicate. */
|
|
4542
|
+
export function browserFileAuthoritySubjectId(
|
|
4543
|
+
grant: AccessGrant,
|
|
4544
|
+
authorization: ResolvedSessionAuthorization | null,
|
|
4545
|
+
): string | null {
|
|
4546
|
+
if (!authorization) return grant.principalKind === "agent_attempt" ? null : grant.subjectId;
|
|
4547
|
+
return authorization.actor.kind === "agent_attempt"
|
|
4548
|
+
? authorization.actor.initiatingHumanSubjectId
|
|
4549
|
+
: authorization.actor.subjectId;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
/** The batch authority query intentionally omits every unauthorized file. Any
|
|
4553
|
+
* omission therefore fails the whole upload before an object-storage URL is
|
|
4554
|
+
* minted, including mixed ordinary/Drive mappings and partially authorized
|
|
4555
|
+
* batches. */
|
|
4556
|
+
export function requireAuthorizedBrowserUploadFiles(
|
|
4557
|
+
workspaceFileIds: readonly string[],
|
|
4558
|
+
authorizedFiles: readonly FileAsset[],
|
|
4559
|
+
): FileAsset[] {
|
|
4560
|
+
const byId = new Map(authorizedFiles.map((file) => [file.id, file]));
|
|
4561
|
+
return workspaceFileIds.map((fileId) => {
|
|
4562
|
+
const file = byId.get(fileId);
|
|
4563
|
+
if (!file) throw new HTTPException(404, { message: "workspace file not found" });
|
|
4564
|
+
if (file.status !== "ready") {
|
|
4565
|
+
throw new HTTPException(409, { message: "workspace file is not ready" });
|
|
4566
|
+
}
|
|
4567
|
+
return file;
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4288
4571
|
function browserUploadFilename(value: string): string {
|
|
4289
4572
|
const normalized = value
|
|
4290
4573
|
.trim()
|
|
@@ -4370,9 +4653,14 @@ async function finalizeBrowserDownloadFile(
|
|
|
4370
4653
|
if (upload.expiresAt.getTime() <= Date.now()) {
|
|
4371
4654
|
throw new InteractionResourceStateError("Browser download file upload authority expired");
|
|
4372
4655
|
}
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4656
|
+
const pendingFile = upload.file;
|
|
4657
|
+
const head = await retryWhileMissing(async () => {
|
|
4658
|
+
if (!(await objectStorage.fileExists(pendingFile))) return null;
|
|
4659
|
+
return await objectStorage.headFile(pendingFile);
|
|
4375
4660
|
});
|
|
4661
|
+
if (!head) {
|
|
4662
|
+
throw new HTTPException(503, { message: "browser download object is not available" });
|
|
4663
|
+
}
|
|
4376
4664
|
if (
|
|
4377
4665
|
Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
|
|
4378
4666
|
(head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
|
|
@@ -4413,6 +4701,8 @@ async function recordBrowserDownloadFileUsage(
|
|
|
4413
4701
|
}
|
|
4414
4702
|
|
|
4415
4703
|
function browserRouteError(error: unknown): HTTPException {
|
|
4704
|
+
const connectedMachineError = interactionControlApiError(error, "browser");
|
|
4705
|
+
if (connectedMachineError) return connectedMachineError;
|
|
4416
4706
|
if (error instanceof HTTPException) return error;
|
|
4417
4707
|
if (error instanceof BrowserSessionNotFoundError) {
|
|
4418
4708
|
return new HTTPException(404, { message: error.message, cause: error });
|