@opengeni/api-router 0.30.3 → 2.2.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-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.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-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 +326 -82
- 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 +18 -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 +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- 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 +4 -1
- 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/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 +161 -23
- 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 +28 -3
- 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 +301 -92
- 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 +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -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 +16 -1
- 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 +329 -79
- 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 +144 -34
- package/src/routes/documents.ts +353 -4
- 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 +72 -11
- 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 +775 -38
- 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-WCXHI3FF.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,10 +96,11 @@ 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,
|
|
@@ -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,6 +145,7 @@ import {
|
|
|
142
145
|
recordWorkspaceUsage,
|
|
143
146
|
requireLimit,
|
|
144
147
|
relayConfigFromSettings,
|
|
148
|
+
resolveSessionSandboxRuntime,
|
|
145
149
|
SessionAuthorizationDeniedError,
|
|
146
150
|
SessionAuthorizationUnavailableError,
|
|
147
151
|
type ApiRouteDeps,
|
|
@@ -158,16 +162,19 @@ import {
|
|
|
158
162
|
buildStreamUrl,
|
|
159
163
|
exposedPortEndpointFromUrl,
|
|
160
164
|
buildSelfhostedBackendSession,
|
|
165
|
+
ensureDisplayStack,
|
|
161
166
|
mintStreamToken,
|
|
162
167
|
NatsControlRpc,
|
|
163
168
|
NatsOpStreamTransport,
|
|
164
169
|
provisionBrowserControlClient,
|
|
170
|
+
renewSandboxProviderExpiration,
|
|
165
171
|
type BrowserControlPlacementSession,
|
|
166
172
|
type PlacementBrowserStateCaptureReceipt,
|
|
167
173
|
type PlacementBrowserNetworkRoute,
|
|
168
174
|
type PlacementBrowserTransport,
|
|
169
175
|
type RestorePlacementBrowserStateInput,
|
|
170
176
|
} from "@opengeni/runtime/sandbox";
|
|
177
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
171
178
|
import type { Context, Hono } from "hono";
|
|
172
179
|
import { HTTPException } from "hono/http-exception";
|
|
173
180
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
@@ -176,8 +183,14 @@ import {
|
|
|
176
183
|
deriveBrowserNetworkRouteAuthorityDigest,
|
|
177
184
|
deriveBrowserSessionControllerTokens,
|
|
178
185
|
deriveBrowserViewGrantToken,
|
|
186
|
+
deriveComputerSessionControllerTokens,
|
|
179
187
|
} from "../browser-controller-authority";
|
|
180
|
-
import {
|
|
188
|
+
import {
|
|
189
|
+
controllerCacheAllowsHostFetch,
|
|
190
|
+
controllerCachedUrlIsUsable,
|
|
191
|
+
shouldPersistControllerDataPlaneUrl,
|
|
192
|
+
withCachedController,
|
|
193
|
+
} from "../controller-data-plane";
|
|
181
194
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
182
195
|
import {
|
|
183
196
|
browserStateArtifactAad,
|
|
@@ -192,7 +205,13 @@ import {
|
|
|
192
205
|
resolveProtectedAuthFieldValues,
|
|
193
206
|
} from "../browser-auth-broker";
|
|
194
207
|
import { managedNetworkRouteForPlacement } from "../browser-network-route";
|
|
195
|
-
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";
|
|
196
215
|
import {
|
|
197
216
|
observeAuthMutation,
|
|
198
217
|
observeBrowserActionResult,
|
|
@@ -298,7 +317,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
298
317
|
const request = await parseJsonBody(context, CreateBrowserSessionRequest);
|
|
299
318
|
const startedAtMs = performance.now();
|
|
300
319
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
301
|
-
const origin = requestOrigin(context, deps.settings
|
|
320
|
+
const origin = requestOrigin(context, deps.settings);
|
|
302
321
|
const authority = browserAuthorityRoot(deps);
|
|
303
322
|
|
|
304
323
|
try {
|
|
@@ -325,6 +344,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
325
344
|
return context.json(parsed, 200);
|
|
326
345
|
}
|
|
327
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
|
+
|
|
328
361
|
const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
|
|
329
362
|
const response = await withBrowserPlacement(
|
|
330
363
|
sourceSession,
|
|
@@ -401,12 +434,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
401
434
|
});
|
|
402
435
|
const preparedSession = prepared.session;
|
|
403
436
|
const controllerGeneration = requireOperationGeneration(record);
|
|
404
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
405
|
-
deps,
|
|
406
|
-
grant,
|
|
407
|
-
prepared.session,
|
|
408
|
-
placement,
|
|
409
|
-
);
|
|
410
437
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
411
438
|
rootSecret: authority,
|
|
412
439
|
accountId: grant.accountId,
|
|
@@ -432,8 +459,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
432
459
|
placement,
|
|
433
460
|
adminToken,
|
|
434
461
|
origin,
|
|
435
|
-
async (client) =>
|
|
436
|
-
await
|
|
462
|
+
async (client) => {
|
|
463
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
464
|
+
deps,
|
|
465
|
+
grant,
|
|
466
|
+
preparedSession,
|
|
467
|
+
placement,
|
|
468
|
+
client,
|
|
469
|
+
);
|
|
470
|
+
return await withInteractionHolderHeartbeat(
|
|
437
471
|
deps,
|
|
438
472
|
{
|
|
439
473
|
grant,
|
|
@@ -444,8 +478,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
444
478
|
resourceId: preparedSession.id,
|
|
445
479
|
controllerGeneration,
|
|
446
480
|
},
|
|
447
|
-
async () =>
|
|
448
|
-
await
|
|
481
|
+
async () => {
|
|
482
|
+
await ensureHeadedBrowserDisplayStack(
|
|
483
|
+
placement.session,
|
|
484
|
+
preparedSession.headless,
|
|
485
|
+
linkedComputer !== null,
|
|
486
|
+
);
|
|
487
|
+
return await client.createSession({
|
|
449
488
|
browserSessionId: preparedSession.id,
|
|
450
489
|
controllerGeneration,
|
|
451
490
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -456,8 +495,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
456
495
|
...(networkRoute ? { networkRoute } : {}),
|
|
457
496
|
...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
|
|
458
497
|
...(restore ? { restore } : {}),
|
|
459
|
-
})
|
|
460
|
-
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
},
|
|
461
502
|
);
|
|
462
503
|
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
463
504
|
() => placement,
|
|
@@ -1663,7 +1704,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1663
1704
|
context,
|
|
1664
1705
|
"stream:view",
|
|
1665
1706
|
);
|
|
1666
|
-
const origin = requestOrigin(context, deps.settings
|
|
1707
|
+
const origin = requestOrigin(context, deps.settings);
|
|
1667
1708
|
const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
|
|
1668
1709
|
const result = await withActiveBrowserController(
|
|
1669
1710
|
context,
|
|
@@ -1719,6 +1760,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1719
1760
|
});
|
|
1720
1761
|
const stream = relayed
|
|
1721
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
|
+
}
|
|
1722
1775
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
1723
1776
|
workspaceId,
|
|
1724
1777
|
sessionId: record.sourceSessionId,
|
|
@@ -1726,6 +1779,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1726
1779
|
leaseEpoch: record.tokenGeneration,
|
|
1727
1780
|
port: relayed.channel.port,
|
|
1728
1781
|
ttlSeconds: request.expiresInSeconds,
|
|
1782
|
+
subjectId: grant.subjectId,
|
|
1783
|
+
authorityEpoch: relayAuthorityEpoch,
|
|
1729
1784
|
});
|
|
1730
1785
|
return {
|
|
1731
1786
|
kind: "relay" as const,
|
|
@@ -1740,14 +1795,44 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1740
1795
|
},
|
|
1741
1796
|
};
|
|
1742
1797
|
})()
|
|
1743
|
-
: {
|
|
1744
|
-
|
|
1745
|
-
url: await client.frameStreamUrl(reference, request.targetId, request.stream),
|
|
1746
|
-
protocols: [
|
|
1798
|
+
: await (async () => {
|
|
1799
|
+
const protocols = [
|
|
1747
1800
|
BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
1748
1801
|
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
1749
|
-
]
|
|
1750
|
-
|
|
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
|
+
})();
|
|
1751
1836
|
return BrowserSessionAttachment.parse({
|
|
1752
1837
|
browserSessionId,
|
|
1753
1838
|
controllerGeneration: binding.controllerGeneration,
|
|
@@ -1958,7 +2043,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1958
2043
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
1959
2044
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
1960
2045
|
const startedAtMs = performance.now();
|
|
1961
|
-
const origin = requestOrigin(context, deps.settings
|
|
2046
|
+
const origin = requestOrigin(context, deps.settings);
|
|
1962
2047
|
try {
|
|
1963
2048
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
1964
2049
|
accountId: grant.accountId,
|
|
@@ -2184,7 +2269,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2184
2269
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2185
2270
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2186
2271
|
const startedAtMs = performance.now();
|
|
2187
|
-
const origin = requestOrigin(context, deps.settings
|
|
2272
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2188
2273
|
let restore: RestorePlacementBrowserStateInput | null = null;
|
|
2189
2274
|
try {
|
|
2190
2275
|
let before = await getBrowserSessionControlRecord(deps.db, {
|
|
@@ -2303,12 +2388,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2303
2388
|
throw error;
|
|
2304
2389
|
});
|
|
2305
2390
|
const controllerGeneration = requireOperationGeneration(record);
|
|
2306
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
2307
|
-
deps,
|
|
2308
|
-
grant,
|
|
2309
|
-
prepared.session,
|
|
2310
|
-
placement,
|
|
2311
|
-
);
|
|
2312
2391
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
2313
2392
|
rootSecret: authority,
|
|
2314
2393
|
accountId: grant.accountId,
|
|
@@ -2332,6 +2411,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2332
2411
|
...(origin ? { allowedOrigins: [origin] } : {}),
|
|
2333
2412
|
});
|
|
2334
2413
|
try {
|
|
2414
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
2415
|
+
deps,
|
|
2416
|
+
grant,
|
|
2417
|
+
prepared.session,
|
|
2418
|
+
placement,
|
|
2419
|
+
client,
|
|
2420
|
+
);
|
|
2335
2421
|
await withInteractionHolderHeartbeat(
|
|
2336
2422
|
deps,
|
|
2337
2423
|
{
|
|
@@ -2343,8 +2429,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2343
2429
|
resourceId: browserSessionId,
|
|
2344
2430
|
controllerGeneration,
|
|
2345
2431
|
},
|
|
2346
|
-
async () =>
|
|
2347
|
-
await
|
|
2432
|
+
async () => {
|
|
2433
|
+
await ensureHeadedBrowserDisplayStack(
|
|
2434
|
+
placement.session,
|
|
2435
|
+
prepared.session.headless,
|
|
2436
|
+
linkedComputer !== null,
|
|
2437
|
+
);
|
|
2438
|
+
return await client.createSession({
|
|
2348
2439
|
browserSessionId,
|
|
2349
2440
|
controllerGeneration,
|
|
2350
2441
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -2354,7 +2445,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2354
2445
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
2355
2446
|
...(networkRoute ? { networkRoute } : {}),
|
|
2356
2447
|
restore: restore!,
|
|
2357
|
-
})
|
|
2448
|
+
});
|
|
2449
|
+
},
|
|
2358
2450
|
);
|
|
2359
2451
|
} catch (error) {
|
|
2360
2452
|
if (!isDefiniteBrowserControllerFailure(error)) throw error;
|
|
@@ -2426,7 +2518,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2426
2518
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2427
2519
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2428
2520
|
const startedAtMs = performance.now();
|
|
2429
|
-
const origin = requestOrigin(context, deps.settings
|
|
2521
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2430
2522
|
try {
|
|
2431
2523
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
2432
2524
|
accountId: grant.accountId,
|
|
@@ -2573,18 +2665,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2573
2665
|
if (device.state !== "connected") {
|
|
2574
2666
|
throw new BrowserSessionStateError("Attached browser is disconnected");
|
|
2575
2667
|
}
|
|
2576
|
-
const enrollment = await
|
|
2668
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
2577
2669
|
deps.db,
|
|
2578
2670
|
sourceSession.workspaceId,
|
|
2579
2671
|
device.enrollmentId,
|
|
2580
2672
|
);
|
|
2581
|
-
if (!enrollment || enrollment.status !== "active") {
|
|
2673
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
2582
2674
|
throw new BrowserSessionStateError("Attached browser machine is unavailable");
|
|
2583
2675
|
}
|
|
2584
|
-
|
|
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
|
+
);
|
|
2585
2686
|
const built = await buildSelfhostedBackendSession({
|
|
2586
2687
|
workspaceId: sourceSession.workspaceId,
|
|
2587
2688
|
agentId: device.enrollmentId,
|
|
2689
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
2690
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
2588
2691
|
relay: relayConfigFromSettings(deps.settings),
|
|
2589
2692
|
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
2590
2693
|
// Browser-profile generation is the physical controller fence. Epoch 0
|
|
@@ -2593,6 +2696,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2593
2696
|
epoch: 0,
|
|
2594
2697
|
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
2595
2698
|
execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
|
|
2699
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
2700
|
+
operationResourcePolicySupported:
|
|
2701
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
2702
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
2596
2703
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
2597
2704
|
enrollment.opStream === true &&
|
|
2598
2705
|
deps.bus.getOpStreamConnection
|
|
@@ -2609,13 +2716,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2609
2716
|
return await callback({
|
|
2610
2717
|
placement: expectedPlacement,
|
|
2611
2718
|
controllerHostSandboxGroupId: null,
|
|
2612
|
-
placementInstanceId
|
|
2719
|
+
placementInstanceId,
|
|
2613
2720
|
session: built.session as unknown as BrowserControlPlacementSession,
|
|
2614
2721
|
lease: null,
|
|
2615
2722
|
transport: {
|
|
2616
2723
|
kind: "attached_chrome",
|
|
2617
2724
|
deviceId: device.id,
|
|
2618
|
-
connectionGeneration:
|
|
2725
|
+
connectionGeneration: placementInstanceId,
|
|
2619
2726
|
browserName: device.browserName,
|
|
2620
2727
|
browserVersion: device.browserVersion,
|
|
2621
2728
|
},
|
|
@@ -2681,8 +2788,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2681
2788
|
resolved.kind !== "selfhosted" ||
|
|
2682
2789
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
2683
2790
|
) {
|
|
2684
|
-
|
|
2685
|
-
|
|
2791
|
+
return await throwBrowserSourcePlacementChanged(
|
|
2792
|
+
grant,
|
|
2793
|
+
sourceSession.id,
|
|
2794
|
+
expectedPlacement.sandboxId,
|
|
2686
2795
|
);
|
|
2687
2796
|
}
|
|
2688
2797
|
const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
|
|
@@ -2779,7 +2888,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2779
2888
|
!lease ||
|
|
2780
2889
|
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
2781
2890
|
lease.instanceId !== binding.placementInstanceId ||
|
|
2782
|
-
!lease.controllerDataPlaneUrl
|
|
2891
|
+
!lease.controllerDataPlaneUrl ||
|
|
2892
|
+
(lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
|
|
2893
|
+
!controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
|
|
2783
2894
|
) {
|
|
2784
2895
|
return null;
|
|
2785
2896
|
}
|
|
@@ -2805,8 +2916,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2805
2916
|
if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
|
|
2806
2917
|
return placement;
|
|
2807
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
|
+
}
|
|
2808
2931
|
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
2809
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
|
+
}
|
|
2810
2942
|
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2811
2943
|
accountId: grant.accountId,
|
|
2812
2944
|
workspaceId,
|
|
@@ -2816,6 +2948,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2816
2948
|
controllerDataPlaneUrl: url,
|
|
2817
2949
|
});
|
|
2818
2950
|
if (!lease) return placement;
|
|
2951
|
+
if (!controllerCacheAllowsHostFetch(url)) {
|
|
2952
|
+
return { ...placement, lease };
|
|
2953
|
+
}
|
|
2819
2954
|
return { ...placement, session: controllerOnlySession(url), lease };
|
|
2820
2955
|
}
|
|
2821
2956
|
|
|
@@ -2831,7 +2966,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2831
2966
|
const sandboxGroupId = placement.controllerHostSandboxGroupId;
|
|
2832
2967
|
const cachedUrl = placement.lease?.controllerDataPlaneUrl;
|
|
2833
2968
|
return await withCachedController({
|
|
2834
|
-
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,
|
|
2835
2979
|
createCachedClient: (url) =>
|
|
2836
2980
|
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
2837
2981
|
prepareCachedClient: async (client) => {
|
|
@@ -2889,6 +3033,17 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2889
3033
|
record.sourceSessionId,
|
|
2890
3034
|
authorizationOperation,
|
|
2891
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
|
+
}
|
|
2892
3047
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
2893
3048
|
throw new BrowserSessionStateError("BrowserSession is not active");
|
|
2894
3049
|
}
|
|
@@ -2973,7 +3128,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2973
3128
|
}
|
|
2974
3129
|
}
|
|
2975
3130
|
|
|
2976
|
-
|
|
3131
|
+
sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
2977
3132
|
return await withBrowserPlacement(
|
|
2978
3133
|
sourceSession,
|
|
2979
3134
|
grant,
|
|
@@ -2993,6 +3148,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2993
3148
|
}
|
|
2994
3149
|
}
|
|
2995
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
|
+
|
|
2996
3170
|
async function recoverActiveBrowserController(
|
|
2997
3171
|
routeDeps: ApiRouteDeps,
|
|
2998
3172
|
grant: AccessGrant,
|
|
@@ -3002,11 +3176,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3002
3176
|
client: BrowserControlClient,
|
|
3003
3177
|
tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
|
|
3004
3178
|
): Promise<void> {
|
|
3005
|
-
const linkedComputer = await
|
|
3179
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
3006
3180
|
routeDeps,
|
|
3007
3181
|
grant,
|
|
3008
3182
|
record.session,
|
|
3009
3183
|
placement,
|
|
3184
|
+
client,
|
|
3010
3185
|
);
|
|
3011
3186
|
const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
|
|
3012
3187
|
deps: routeDeps,
|
|
@@ -3015,6 +3190,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3015
3190
|
placement: placement.placement,
|
|
3016
3191
|
});
|
|
3017
3192
|
try {
|
|
3193
|
+
await ensureHeadedBrowserDisplayStack(
|
|
3194
|
+
placement.session,
|
|
3195
|
+
record.session.headless,
|
|
3196
|
+
linkedComputer !== null,
|
|
3197
|
+
);
|
|
3018
3198
|
await client.createSession({
|
|
3019
3199
|
browserSessionId: record.session.id,
|
|
3020
3200
|
controllerGeneration: binding.controllerGeneration,
|
|
@@ -3065,7 +3245,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3065
3245
|
grant,
|
|
3066
3246
|
record,
|
|
3067
3247
|
placement,
|
|
3068
|
-
requestOrigin(context, deps.settings
|
|
3248
|
+
requestOrigin(context, deps.settings),
|
|
3069
3249
|
);
|
|
3070
3250
|
await endCapturedBrowserController(client, browserSessionId, binding);
|
|
3071
3251
|
await clearSuspendedBrowserSessionController(deps.db, {
|
|
@@ -3087,6 +3267,21 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3087
3267
|
}
|
|
3088
3268
|
}
|
|
3089
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
|
+
|
|
3090
3285
|
function browserCreateInput(
|
|
3091
3286
|
grant: AccessGrant,
|
|
3092
3287
|
workspaceId: string,
|
|
@@ -3235,11 +3430,31 @@ function assertCreateReplay(
|
|
|
3235
3430
|
}
|
|
3236
3431
|
}
|
|
3237
3432
|
|
|
3238
|
-
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(
|
|
3239
3453
|
deps: ApiRouteDeps,
|
|
3240
3454
|
grant: AccessGrant,
|
|
3241
3455
|
browser: BrowserSessionValue,
|
|
3242
3456
|
placement: BrowserPlacement,
|
|
3457
|
+
client: BrowserControlClient,
|
|
3243
3458
|
): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
|
|
3244
3459
|
if (!browser.linkedComputerSessionId) return null;
|
|
3245
3460
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -3257,10 +3472,42 @@ async function requireLinkedComputerBinding(
|
|
|
3257
3472
|
"Linked ComputerSession is not active on the browser placement",
|
|
3258
3473
|
);
|
|
3259
3474
|
}
|
|
3260
|
-
|
|
3475
|
+
const reference = {
|
|
3261
3476
|
computerSessionId: record.session.id,
|
|
3262
3477
|
controllerGeneration: record.session.controller.controllerGeneration,
|
|
3263
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
|
+
);
|
|
3264
3511
|
}
|
|
3265
3512
|
|
|
3266
3513
|
async function requireBrowserRevisionRestoreAuthority(
|
|
@@ -3480,6 +3727,7 @@ async function ensureInteractionHolder(
|
|
|
3480
3727
|
if (!placement.lease?.instanceId) {
|
|
3481
3728
|
throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
|
|
3482
3729
|
}
|
|
3730
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
|
|
3483
3731
|
const acquired = await acquireLease(deps.db, {
|
|
3484
3732
|
accountId: grant.accountId,
|
|
3485
3733
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -3489,8 +3737,8 @@ async function ensureInteractionHolder(
|
|
|
3489
3737
|
subjectId: sourceSession.id,
|
|
3490
3738
|
backend: placement.lease.backend,
|
|
3491
3739
|
os: placement.lease.os,
|
|
3492
|
-
image:
|
|
3493
|
-
rigVersionId:
|
|
3740
|
+
image: sandboxRuntime.image,
|
|
3741
|
+
rigVersionId: sourceSession.rigVersionId,
|
|
3494
3742
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
3495
3743
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
3496
3744
|
waitSignal,
|
|
@@ -3512,6 +3760,11 @@ async function ensureInteractionHolder(
|
|
|
3512
3760
|
).catch(() => undefined);
|
|
3513
3761
|
throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
|
|
3514
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);
|
|
3515
3768
|
return true;
|
|
3516
3769
|
}
|
|
3517
3770
|
|
|
@@ -3902,32 +4155,8 @@ function isUuid(value: unknown): value is string {
|
|
|
3902
4155
|
);
|
|
3903
4156
|
}
|
|
3904
4157
|
|
|
3905
|
-
function requestOrigin(context: Context,
|
|
3906
|
-
return
|
|
3907
|
-
}
|
|
3908
|
-
|
|
3909
|
-
export function validateBrowserRequestOrigin(
|
|
3910
|
-
value: string | undefined,
|
|
3911
|
-
allowedPattern: string,
|
|
3912
|
-
): string | null {
|
|
3913
|
-
if (!value) return null;
|
|
3914
|
-
let url: URL;
|
|
3915
|
-
try {
|
|
3916
|
-
url = new URL(value);
|
|
3917
|
-
} catch {
|
|
3918
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3919
|
-
}
|
|
3920
|
-
if (
|
|
3921
|
-
url.origin === "null" ||
|
|
3922
|
-
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
3923
|
-
url.origin !== value
|
|
3924
|
-
) {
|
|
3925
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3926
|
-
}
|
|
3927
|
-
if (!allowedCorsOrigin(allowedPattern, url.origin)) {
|
|
3928
|
-
throw new HTTPException(403, { message: "request origin is not allowed" });
|
|
3929
|
-
}
|
|
3930
|
-
return url.origin;
|
|
4158
|
+
function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
|
|
4159
|
+
return validateInteractionRequestOrigin(context.req.header("origin"), settings);
|
|
3931
4160
|
}
|
|
3932
4161
|
|
|
3933
4162
|
export function interactionActorForGrant(
|
|
@@ -3955,6 +4184,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
|
|
|
3955
4184
|
}
|
|
3956
4185
|
}
|
|
3957
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
|
+
|
|
3958
4201
|
function isTerminalOperation(state: string): boolean {
|
|
3959
4202
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
3960
4203
|
}
|
|
@@ -4410,9 +4653,14 @@ async function finalizeBrowserDownloadFile(
|
|
|
4410
4653
|
if (upload.expiresAt.getTime() <= Date.now()) {
|
|
4411
4654
|
throw new InteractionResourceStateError("Browser download file upload authority expired");
|
|
4412
4655
|
}
|
|
4413
|
-
const
|
|
4414
|
-
|
|
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);
|
|
4415
4660
|
});
|
|
4661
|
+
if (!head) {
|
|
4662
|
+
throw new HTTPException(503, { message: "browser download object is not available" });
|
|
4663
|
+
}
|
|
4416
4664
|
if (
|
|
4417
4665
|
Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
|
|
4418
4666
|
(head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
|
|
@@ -4453,6 +4701,8 @@ async function recordBrowserDownloadFileUsage(
|
|
|
4453
4701
|
}
|
|
4454
4702
|
|
|
4455
4703
|
function browserRouteError(error: unknown): HTTPException {
|
|
4704
|
+
const connectedMachineError = interactionControlApiError(error, "browser");
|
|
4705
|
+
if (connectedMachineError) return connectedMachineError;
|
|
4456
4706
|
if (error instanceof HTTPException) return error;
|
|
4457
4707
|
if (error instanceof BrowserSessionNotFoundError) {
|
|
4458
4708
|
return new HTTPException(404, { message: error.message, cause: error });
|