@opengeni/api-router 0.26.1 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +1 -0
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +19 -1
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.js +1 -1
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/google-drive.d.ts +6 -6
- package/dist/integrations/provider-oauth.d.ts +0 -2
- package/dist/integrations/slack-bot.d.ts +171 -1
- package/dist/integrations/slack-interactions.d.ts +17 -5
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/server.d.ts +3 -2
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/sessions.d.ts +2 -1
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +8 -1
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/viewer.d.ts +3 -1
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +17 -16
- package/src/app.ts +352 -49
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +50 -1
- package/src/browser-network-route.ts +34 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/http/auth.ts +27 -9
- package/src/http/sse.ts +229 -5
- package/src/integrations/api-integrations.ts +57 -42
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/google-drive.ts +31 -26
- package/src/integrations/provider-oauth.ts +70 -91
- package/src/integrations/slack-bot.ts +638 -9
- package/src/integrations/slack-interactions.ts +1583 -73
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +115 -6
- package/src/mcp/server.ts +865 -242
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +32 -29
- package/src/routes/browser-identities.ts +24 -0
- package/src/routes/browser-sessions.ts +2122 -183
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/computer-sessions.ts +441 -86
- package/src/routes/connections.ts +141 -4
- package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/plugins.ts +2 -1
- package/src/routes/sessions.ts +419 -43
- package/src/routes/skills.ts +92 -10
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +34 -4
- package/src/routes/workspaces.ts +22 -4
- package/src/sandbox/channel-a.ts +29 -2
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/viewer.ts +10 -3
- package/src/slack-reaction-files.ts +181 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-JIKNR5YL.js.map +0 -1
- package/dist/routes/integration-features.d.ts +0 -3
|
@@ -2,7 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { resolveFirstPartyDelegationSecret, resolveStreamTokenSecret } from "@opengeni/config";
|
|
3
3
|
import {
|
|
4
4
|
BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
|
|
5
|
+
BROWSER_CONTROL_PORT,
|
|
5
6
|
COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
7
|
+
COMPUTER_RFB_WEBSOCKET_PROTOCOL,
|
|
6
8
|
ComputerActionCommand,
|
|
7
9
|
ComputerActionRequest,
|
|
8
10
|
ComputerSessionAttachment,
|
|
@@ -32,12 +34,16 @@ import {
|
|
|
32
34
|
dispatchComputerSessionOperation,
|
|
33
35
|
failComputerSessionOperation,
|
|
34
36
|
findComputerSessionControlRecordByOperation,
|
|
37
|
+
getAttachedBrowserDevice,
|
|
35
38
|
getComputerSessionControlRecord,
|
|
39
|
+
getEnrollment,
|
|
36
40
|
getSession,
|
|
37
41
|
listComputerSessions,
|
|
38
42
|
prepareComputerSessionCreate,
|
|
39
43
|
prepareComputerSessionEnd,
|
|
40
44
|
releaseLeaseHolder,
|
|
45
|
+
readLease,
|
|
46
|
+
recordLeaseControllerDataPlaneUrl,
|
|
41
47
|
touchComputerSessionController,
|
|
42
48
|
type ComputerSessionControlRecord,
|
|
43
49
|
type LeaseSnapshot,
|
|
@@ -45,21 +51,26 @@ import {
|
|
|
45
51
|
import {
|
|
46
52
|
requireAccessGrant,
|
|
47
53
|
requireSessionAuthorization,
|
|
54
|
+
relayConfigFromSettings,
|
|
48
55
|
SessionAuthorizationDeniedError,
|
|
49
56
|
SessionAuthorizationUnavailableError,
|
|
50
57
|
type ApiRouteDeps,
|
|
51
58
|
} from "@opengeni/core";
|
|
52
59
|
import {
|
|
60
|
+
BrowserControlClient,
|
|
53
61
|
BrowserControlProtocolError,
|
|
54
62
|
BrowserControlRequestError,
|
|
55
63
|
BrowserControlServerError,
|
|
56
64
|
BrowserControlServerUnsupportedError,
|
|
57
65
|
BrowserControlTransportError,
|
|
58
66
|
BrowserControlUnsupportedError,
|
|
67
|
+
buildSelfhostedBackendSession,
|
|
59
68
|
buildStreamUrl,
|
|
69
|
+
exposedPortEndpointFromUrl,
|
|
60
70
|
mintStreamToken,
|
|
71
|
+
NatsControlRpc,
|
|
72
|
+
NatsOpStreamTransport,
|
|
61
73
|
provisionBrowserControlClient,
|
|
62
|
-
type BrowserControlClient,
|
|
63
74
|
type BrowserControlPlacementSession,
|
|
64
75
|
} from "@opengeni/runtime/sandbox";
|
|
65
76
|
import type { Context, Hono } from "hono";
|
|
@@ -70,8 +81,11 @@ import {
|
|
|
70
81
|
deriveComputerSessionControllerTokens,
|
|
71
82
|
deriveComputerViewGrantToken,
|
|
72
83
|
} from "../browser-controller-authority";
|
|
84
|
+
import { withCachedController } from "../controller-data-plane";
|
|
85
|
+
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
73
86
|
import { allowedCorsOrigin } from "../http/cors";
|
|
74
|
-
import {
|
|
87
|
+
import { observeComputerActionResult, observeLifecycleResult } from "../interaction-metrics";
|
|
88
|
+
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
75
89
|
|
|
76
90
|
type ComputerPlacement = {
|
|
77
91
|
placement: InteractionPlacement;
|
|
@@ -125,6 +139,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
125
139
|
const workspaceId = context.req.param("workspaceId") ?? "";
|
|
126
140
|
const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
|
|
127
141
|
const request = await parseJsonBody(context, CreateComputerSessionRequest);
|
|
142
|
+
const startedAtMs = performance.now();
|
|
128
143
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
129
144
|
const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
|
|
130
145
|
const authority = controllerAuthorityRoot(deps);
|
|
@@ -148,7 +163,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
148
163
|
)
|
|
149
164
|
: null;
|
|
150
165
|
if (prepared && isTerminalOperation(prepared.operation.state)) {
|
|
151
|
-
|
|
166
|
+
const parsed = ComputerSessionMutationResponse.parse(prepared);
|
|
167
|
+
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
168
|
+
return context.json(parsed, 200);
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
|
|
@@ -181,7 +198,18 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
181
198
|
prepared.session.id,
|
|
182
199
|
request.operationId,
|
|
183
200
|
placement.placementInstanceId,
|
|
184
|
-
)
|
|
201
|
+
).catch(async (error: unknown) => {
|
|
202
|
+
if (interactionHeld) {
|
|
203
|
+
await releaseInteractionHolder(
|
|
204
|
+
grant,
|
|
205
|
+
workspaceId,
|
|
206
|
+
prepared!.session.id,
|
|
207
|
+
placement.placement,
|
|
208
|
+
).catch(() => undefined);
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
});
|
|
212
|
+
const preparedSession = prepared.session;
|
|
185
213
|
const controllerGeneration = requireOperationGeneration(record);
|
|
186
214
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
187
215
|
rootSecret: authority,
|
|
@@ -202,21 +230,45 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
202
230
|
});
|
|
203
231
|
let physical;
|
|
204
232
|
try {
|
|
205
|
-
|
|
233
|
+
physical = await withComputerCreationController(
|
|
234
|
+
grant,
|
|
235
|
+
workspaceId,
|
|
236
|
+
placement,
|
|
206
237
|
adminToken,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
origin,
|
|
239
|
+
async (client) =>
|
|
240
|
+
await withInteractionHolderHeartbeat(
|
|
241
|
+
deps,
|
|
242
|
+
{
|
|
243
|
+
grant,
|
|
244
|
+
workspaceId,
|
|
245
|
+
sandboxGroupId:
|
|
246
|
+
placement.placement.kind === "sandbox_group"
|
|
247
|
+
? placement.placement.sandboxGroupId
|
|
248
|
+
: null,
|
|
249
|
+
holderId: interactionHolderId(preparedSession.id),
|
|
250
|
+
operationId: request.operationId,
|
|
251
|
+
resourceId: preparedSession.id,
|
|
252
|
+
controllerGeneration,
|
|
253
|
+
},
|
|
254
|
+
async () =>
|
|
255
|
+
await client.createComputerSession({
|
|
256
|
+
computerSessionId: preparedSession.id,
|
|
257
|
+
controllerGeneration,
|
|
258
|
+
tokenGeneration: record.tokenGeneration,
|
|
259
|
+
...tokens,
|
|
260
|
+
}),
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
await cacheComputerControllerPlacement(grant, workspaceId, placement).catch(
|
|
264
|
+
() => placement,
|
|
265
|
+
);
|
|
216
266
|
} catch (error) {
|
|
217
267
|
if (
|
|
218
268
|
error instanceof BrowserControlTransportError ||
|
|
219
|
-
(error instanceof BrowserControlRequestError &&
|
|
269
|
+
(error instanceof BrowserControlRequestError &&
|
|
270
|
+
error.retryable &&
|
|
271
|
+
error.error.code !== "machine_locked") ||
|
|
220
272
|
isAbort(error)
|
|
221
273
|
) {
|
|
222
274
|
throw error;
|
|
@@ -225,7 +277,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
225
277
|
accountId: grant.accountId,
|
|
226
278
|
workspaceId,
|
|
227
279
|
operationId: request.operationId,
|
|
228
|
-
computerSessionId:
|
|
280
|
+
computerSessionId: preparedSession.id,
|
|
229
281
|
...(error instanceof BrowserControlProtocolError
|
|
230
282
|
? { state: "outcome_unknown" as const }
|
|
231
283
|
: {}),
|
|
@@ -247,7 +299,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
247
299
|
accountId: grant.accountId,
|
|
248
300
|
workspaceId,
|
|
249
301
|
operationId: request.operationId,
|
|
250
|
-
computerSessionId:
|
|
302
|
+
computerSessionId: preparedSession.id,
|
|
251
303
|
controller: {
|
|
252
304
|
controllerId: "opengeni-browserd",
|
|
253
305
|
controllerGeneration,
|
|
@@ -262,6 +314,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
262
314
|
},
|
|
263
315
|
);
|
|
264
316
|
const parsed = ComputerSessionMutationResponse.parse(response);
|
|
317
|
+
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
265
318
|
return context.json(
|
|
266
319
|
parsed,
|
|
267
320
|
parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
|
|
@@ -317,6 +370,26 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
317
370
|
},
|
|
318
371
|
);
|
|
319
372
|
|
|
373
|
+
app.get(
|
|
374
|
+
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/clipboard",
|
|
375
|
+
async (context) => {
|
|
376
|
+
const { workspaceId, grant, computerSessionId } = await routePreamble(
|
|
377
|
+
context,
|
|
378
|
+
"sessions:read",
|
|
379
|
+
);
|
|
380
|
+
const result = await withActiveComputerController(
|
|
381
|
+
context,
|
|
382
|
+
grant,
|
|
383
|
+
workspaceId,
|
|
384
|
+
computerSessionId,
|
|
385
|
+
"session.read",
|
|
386
|
+
"computer.read",
|
|
387
|
+
async ({ sessionClient }) => await sessionClient.readClipboard(),
|
|
388
|
+
);
|
|
389
|
+
return context.json(result);
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
|
|
320
393
|
app.post(
|
|
321
394
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/actions",
|
|
322
395
|
async (context) => {
|
|
@@ -325,13 +398,14 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
325
398
|
"sessions:control",
|
|
326
399
|
);
|
|
327
400
|
const request = await parseJsonBody(context, ComputerActionRequest);
|
|
401
|
+
const startedAtMs = performance.now();
|
|
328
402
|
const result = await withActiveComputerController(
|
|
329
403
|
context,
|
|
330
404
|
grant,
|
|
331
405
|
workspaceId,
|
|
332
406
|
computerSessionId,
|
|
333
407
|
"session.control",
|
|
334
|
-
"computer.
|
|
408
|
+
"computer.action",
|
|
335
409
|
async ({ sessionClient, binding }) =>
|
|
336
410
|
await sessionClient.action(
|
|
337
411
|
ComputerActionCommand.parse({
|
|
@@ -348,6 +422,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
348
422
|
}),
|
|
349
423
|
),
|
|
350
424
|
);
|
|
425
|
+
observeComputerActionResult(deps.observability, startedAtMs, request, result);
|
|
351
426
|
return context.json(result);
|
|
352
427
|
},
|
|
353
428
|
);
|
|
@@ -377,6 +452,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
377
452
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
|
|
378
453
|
async (context) => {
|
|
379
454
|
const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
|
|
455
|
+
const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
|
|
380
456
|
const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
|
|
381
457
|
const result = await withActiveComputerController(
|
|
382
458
|
context,
|
|
@@ -386,7 +462,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
386
462
|
"session.viewer.read",
|
|
387
463
|
"computer.attach",
|
|
388
464
|
async ({ client, sessionClient, record, binding, placement }) => {
|
|
389
|
-
await
|
|
465
|
+
if (origin) await client.addAllowedOrigins([origin]);
|
|
390
466
|
const grantId = randomUUID();
|
|
391
467
|
const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
|
|
392
468
|
const token = deriveComputerViewGrantToken({
|
|
@@ -406,6 +482,16 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
406
482
|
controllerGeneration: binding.controllerGeneration,
|
|
407
483
|
};
|
|
408
484
|
await client.createComputerViewGrant(reference, { grantId, token, expiresAt });
|
|
485
|
+
const target = (await sessionClient.listTargets()).find(
|
|
486
|
+
(candidate) => candidate.id === request.targetId,
|
|
487
|
+
);
|
|
488
|
+
if (!target) {
|
|
489
|
+
throw new BrowserControlRequestError(404, {
|
|
490
|
+
code: "target_not_found",
|
|
491
|
+
message: "computer target does not exist",
|
|
492
|
+
retryable: false,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
409
495
|
const relaySecret = placement.session.openComputerFrames
|
|
410
496
|
? resolveStreamTokenSecret(deps.settings)
|
|
411
497
|
: null;
|
|
@@ -414,13 +500,23 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
414
500
|
"computer frame relay authority is unavailable",
|
|
415
501
|
);
|
|
416
502
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
503
|
+
let relayed = null;
|
|
504
|
+
try {
|
|
505
|
+
relayed = await client.openRelayedComputerFrameStream({
|
|
506
|
+
reference,
|
|
507
|
+
targetId: request.targetId,
|
|
508
|
+
viewToken: token,
|
|
509
|
+
expiresAt,
|
|
510
|
+
...(request.stream ? { stream: request.stream } : {}),
|
|
511
|
+
});
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error("computer frame relay open failed", {
|
|
514
|
+
computerSessionId,
|
|
515
|
+
targetId: request.targetId,
|
|
516
|
+
failure: error instanceof Error ? error.message : String(error),
|
|
517
|
+
});
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
424
520
|
const stream = relayed
|
|
425
521
|
? await (async () => {
|
|
426
522
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
@@ -444,14 +540,30 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
444
540
|
},
|
|
445
541
|
};
|
|
446
542
|
})()
|
|
447
|
-
:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
543
|
+
: record.session.placement.kind === "sandbox_group" &&
|
|
544
|
+
record.session.platform === "linux" &&
|
|
545
|
+
target.kind === "screen"
|
|
546
|
+
? {
|
|
547
|
+
kind: "direct_rfb" as const,
|
|
548
|
+
url: await client.computerRfbStreamUrl(reference, request.targetId),
|
|
549
|
+
protocols: [
|
|
550
|
+
"binary",
|
|
551
|
+
COMPUTER_RFB_WEBSOCKET_PROTOCOL,
|
|
552
|
+
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
553
|
+
],
|
|
554
|
+
}
|
|
555
|
+
: {
|
|
556
|
+
kind: "direct_websocket" as const,
|
|
557
|
+
url: await client.computerFrameStreamUrl(
|
|
558
|
+
reference,
|
|
559
|
+
request.targetId,
|
|
560
|
+
request.stream,
|
|
561
|
+
),
|
|
562
|
+
protocols: [
|
|
563
|
+
COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
564
|
+
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
565
|
+
],
|
|
566
|
+
};
|
|
455
567
|
return ComputerSessionAttachment.parse({
|
|
456
568
|
computerSessionId,
|
|
457
569
|
controllerGeneration: binding.controllerGeneration,
|
|
@@ -488,6 +600,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
488
600
|
alive: true,
|
|
489
601
|
});
|
|
490
602
|
},
|
|
603
|
+
false,
|
|
491
604
|
);
|
|
492
605
|
return context.json(result);
|
|
493
606
|
},
|
|
@@ -500,6 +613,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
500
613
|
const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
|
|
501
614
|
const computerSessionId = requireUuidParam(context, "computerSessionId");
|
|
502
615
|
const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
|
|
616
|
+
const startedAtMs = performance.now();
|
|
503
617
|
const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
|
|
504
618
|
try {
|
|
505
619
|
const before = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -525,7 +639,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
525
639
|
before.session.placement,
|
|
526
640
|
).catch(() => undefined);
|
|
527
641
|
}
|
|
528
|
-
|
|
642
|
+
const parsed = ComputerSessionMutationResponse.parse(prepared);
|
|
643
|
+
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
644
|
+
return context.json(parsed, 200);
|
|
529
645
|
}
|
|
530
646
|
|
|
531
647
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -549,7 +665,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
549
665
|
computerSessionId,
|
|
550
666
|
record.session.placement,
|
|
551
667
|
).catch(() => undefined);
|
|
552
|
-
|
|
668
|
+
const parsed = ComputerSessionMutationResponse.parse(completed);
|
|
669
|
+
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
670
|
+
return context.json(parsed, 200);
|
|
553
671
|
}
|
|
554
672
|
|
|
555
673
|
const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
@@ -570,12 +688,28 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
570
688
|
});
|
|
571
689
|
const client = await provisionController(grant, record, placement, origin);
|
|
572
690
|
try {
|
|
573
|
-
await
|
|
691
|
+
await withInteractionHolderHeartbeat(
|
|
692
|
+
deps,
|
|
574
693
|
{
|
|
575
|
-
|
|
694
|
+
grant,
|
|
695
|
+
workspaceId,
|
|
696
|
+
sandboxGroupId:
|
|
697
|
+
placement.placement.kind === "sandbox_group"
|
|
698
|
+
? placement.placement.sandboxGroupId
|
|
699
|
+
: null,
|
|
700
|
+
holderId: interactionHolderId(computerSessionId),
|
|
701
|
+
operationId: request.operationId,
|
|
702
|
+
resourceId: computerSessionId,
|
|
576
703
|
controllerGeneration: binding.controllerGeneration,
|
|
577
704
|
},
|
|
578
|
-
|
|
705
|
+
async () =>
|
|
706
|
+
await client.endComputerSession(
|
|
707
|
+
{
|
|
708
|
+
computerSessionId,
|
|
709
|
+
controllerGeneration: binding.controllerGeneration,
|
|
710
|
+
},
|
|
711
|
+
{ removeState: true },
|
|
712
|
+
),
|
|
579
713
|
);
|
|
580
714
|
} catch (error) {
|
|
581
715
|
if (!(error instanceof BrowserControlRequestError && error.status === 404)) {
|
|
@@ -598,7 +732,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
598
732
|
return completed;
|
|
599
733
|
},
|
|
600
734
|
);
|
|
601
|
-
|
|
735
|
+
const parsed = ComputerSessionMutationResponse.parse(response);
|
|
736
|
+
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
737
|
+
return context.json(parsed, 200);
|
|
602
738
|
} catch (error) {
|
|
603
739
|
throw computerRouteError(error);
|
|
604
740
|
}
|
|
@@ -614,15 +750,66 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
614
750
|
waitSignal: AbortSignal,
|
|
615
751
|
callback: (placement: ComputerPlacement) => Promise<T>,
|
|
616
752
|
): Promise<T> {
|
|
617
|
-
if (
|
|
618
|
-
expectedPlacement?.kind === "attached_device" ||
|
|
619
|
-
expectedPlacement?.kind === "external_provider"
|
|
620
|
-
) {
|
|
753
|
+
if (expectedPlacement?.kind === "external_provider") {
|
|
621
754
|
throw new BrowserControlUnsupportedError(
|
|
622
755
|
`computer placement ${expectedPlacement.kind} is not executable`,
|
|
623
756
|
);
|
|
624
757
|
}
|
|
625
|
-
|
|
758
|
+
if (expectedPlacement?.kind === "attached_device") {
|
|
759
|
+
waitSignal.throwIfAborted();
|
|
760
|
+
const device = await getAttachedBrowserDevice(deps.db, {
|
|
761
|
+
accountId: grant.accountId,
|
|
762
|
+
workspaceId: sourceSession.workspaceId,
|
|
763
|
+
deviceId: expectedPlacement.deviceId,
|
|
764
|
+
});
|
|
765
|
+
if (device.state !== "connected") {
|
|
766
|
+
throw new ComputerSessionStateError("Attached browser machine is disconnected");
|
|
767
|
+
}
|
|
768
|
+
const enrollment = await getEnrollment(
|
|
769
|
+
deps.db,
|
|
770
|
+
sourceSession.workspaceId,
|
|
771
|
+
device.enrollmentId,
|
|
772
|
+
);
|
|
773
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
774
|
+
throw new ComputerSessionStateError("Attached browser machine is unavailable");
|
|
775
|
+
}
|
|
776
|
+
assertPlacementInstance(expectedPlacementInstanceId, device.connectionGeneration);
|
|
777
|
+
const built = await buildSelfhostedBackendSession({
|
|
778
|
+
workspaceId: sourceSession.workspaceId,
|
|
779
|
+
agentId: device.enrollmentId,
|
|
780
|
+
relay: relayConfigFromSettings(deps.settings),
|
|
781
|
+
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
782
|
+
epoch: 0,
|
|
783
|
+
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
784
|
+
execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
|
|
785
|
+
...(deps.settings.agentOpStreamEnabled === true &&
|
|
786
|
+
enrollment.opStream === true &&
|
|
787
|
+
deps.bus.getOpStreamConnection
|
|
788
|
+
? {
|
|
789
|
+
opStream: {
|
|
790
|
+
transport: new NatsOpStreamTransport(
|
|
791
|
+
async () => deps.bus.getOpStreamConnection?.() ?? null,
|
|
792
|
+
),
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
: {}),
|
|
796
|
+
});
|
|
797
|
+
waitSignal.throwIfAborted();
|
|
798
|
+
return await callback({
|
|
799
|
+
placement: expectedPlacement,
|
|
800
|
+
placementInstanceId: device.connectionGeneration,
|
|
801
|
+
session: built.session as unknown as BrowserControlPlacementSession,
|
|
802
|
+
lease: null,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
const runWithChannelA =
|
|
806
|
+
operation === "computer.read" ||
|
|
807
|
+
operation === "computer.action" ||
|
|
808
|
+
operation === "computer.control" ||
|
|
809
|
+
operation === "computer.attach"
|
|
810
|
+
? withChannelARead
|
|
811
|
+
: withChannelA;
|
|
812
|
+
return await runWithChannelA(
|
|
626
813
|
channelServices,
|
|
627
814
|
{
|
|
628
815
|
accountId: grant.accountId,
|
|
@@ -631,6 +818,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
631
818
|
subjectId: grant.subjectId,
|
|
632
819
|
waitSignal,
|
|
633
820
|
operation,
|
|
821
|
+
retryControllerTransport: operation === "computer.read" || operation === "computer.action",
|
|
634
822
|
},
|
|
635
823
|
async (handle) => {
|
|
636
824
|
if (expectedPlacement?.kind === "sandbox_group") {
|
|
@@ -703,6 +891,108 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
703
891
|
);
|
|
704
892
|
}
|
|
705
893
|
|
|
894
|
+
function controllerOnlySession(url: string): BrowserControlPlacementSession {
|
|
895
|
+
const endpoint = exposedPortEndpointFromUrl(url);
|
|
896
|
+
return {
|
|
897
|
+
resolveExposedPort: async (port: number) => {
|
|
898
|
+
if (port !== BROWSER_CONTROL_PORT) {
|
|
899
|
+
throw new BrowserControlUnsupportedError(`cached controller cannot expose port ${port}`);
|
|
900
|
+
}
|
|
901
|
+
return endpoint;
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function cachedComputerPlacement(
|
|
907
|
+
record: ComputerSessionControlRecord,
|
|
908
|
+
): Promise<ComputerPlacement | null> {
|
|
909
|
+
const binding = record.session.controller;
|
|
910
|
+
if (!binding || record.session.placement.kind !== "sandbox_group") return null;
|
|
911
|
+
const sandboxGroupId = record.session.placement.sandboxGroupId;
|
|
912
|
+
const lease = await readLease(deps.db, record.session.workspaceId, sandboxGroupId);
|
|
913
|
+
if (
|
|
914
|
+
!lease ||
|
|
915
|
+
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
916
|
+
lease.instanceId !== binding.placementInstanceId ||
|
|
917
|
+
!lease.controllerDataPlaneUrl
|
|
918
|
+
) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
placement: record.session.placement,
|
|
923
|
+
placementInstanceId: binding.placementInstanceId,
|
|
924
|
+
session: controllerOnlySession(lease.controllerDataPlaneUrl),
|
|
925
|
+
lease,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function cacheComputerControllerPlacement(
|
|
930
|
+
grant: AccessGrant,
|
|
931
|
+
workspaceId: string,
|
|
932
|
+
placement: ComputerPlacement,
|
|
933
|
+
): Promise<ComputerPlacement> {
|
|
934
|
+
if (
|
|
935
|
+
placement.placement.kind !== "sandbox_group" ||
|
|
936
|
+
!placement.lease?.instanceId ||
|
|
937
|
+
!placement.session.resolveExposedPort
|
|
938
|
+
) {
|
|
939
|
+
return placement;
|
|
940
|
+
}
|
|
941
|
+
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
942
|
+
const url = buildStreamUrl(endpoint);
|
|
943
|
+
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
944
|
+
accountId: grant.accountId,
|
|
945
|
+
workspaceId,
|
|
946
|
+
sandboxGroupId: placement.placement.sandboxGroupId,
|
|
947
|
+
expectedEpoch: placement.lease.leaseEpoch,
|
|
948
|
+
expectedInstanceId: placement.lease.instanceId,
|
|
949
|
+
controllerDataPlaneUrl: url,
|
|
950
|
+
});
|
|
951
|
+
if (!lease) return placement;
|
|
952
|
+
return { ...placement, session: controllerOnlySession(url), lease };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function withComputerCreationController<T>(
|
|
956
|
+
grant: AccessGrant,
|
|
957
|
+
workspaceId: string,
|
|
958
|
+
placement: ComputerPlacement,
|
|
959
|
+
adminToken: string,
|
|
960
|
+
origin: string | null,
|
|
961
|
+
operation: (client: BrowserControlClient) => Promise<T>,
|
|
962
|
+
): Promise<T> {
|
|
963
|
+
const cachedUrl = placement.lease?.controllerDataPlaneUrl;
|
|
964
|
+
const sandboxGroupId =
|
|
965
|
+
placement.placement.kind === "sandbox_group" ? placement.placement.sandboxGroupId : null;
|
|
966
|
+
return await withCachedController({
|
|
967
|
+
cachedUrl: sandboxGroupId ? (cachedUrl ?? null) : null,
|
|
968
|
+
createCachedClient: (url) =>
|
|
969
|
+
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
970
|
+
prepareCachedClient: async (client) => {
|
|
971
|
+
if (origin) await client.addAllowedOrigins([origin]);
|
|
972
|
+
},
|
|
973
|
+
invalidateCachedUrl: async () => {
|
|
974
|
+
if (!sandboxGroupId || !placement.lease) return;
|
|
975
|
+
await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
976
|
+
accountId: grant.accountId,
|
|
977
|
+
workspaceId,
|
|
978
|
+
sandboxGroupId,
|
|
979
|
+
expectedEpoch: placement.lease.leaseEpoch,
|
|
980
|
+
expectedInstanceId: placement.placementInstanceId,
|
|
981
|
+
controllerDataPlaneUrl: null,
|
|
982
|
+
});
|
|
983
|
+
},
|
|
984
|
+
provisionClient: async () => {
|
|
985
|
+
const { client } = await provisionBrowserControlClient(placement.session, {
|
|
986
|
+
adminToken,
|
|
987
|
+
nativeAuthority: nativeControllerAuthority(workspaceId, placement),
|
|
988
|
+
...(origin ? { allowedOrigins: [origin] } : {}),
|
|
989
|
+
});
|
|
990
|
+
return client;
|
|
991
|
+
},
|
|
992
|
+
use: operation,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
706
996
|
async function withActiveComputerController<T>(
|
|
707
997
|
context: Context,
|
|
708
998
|
grant: AccessGrant,
|
|
@@ -717,6 +1007,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
717
1007
|
binding: NonNullable<ComputerSessionControlRecord["session"]["controller"]>;
|
|
718
1008
|
placement: ComputerPlacement;
|
|
719
1009
|
}) => Promise<T>,
|
|
1010
|
+
recoverMissing = true,
|
|
720
1011
|
): Promise<T> {
|
|
721
1012
|
try {
|
|
722
1013
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -738,6 +1029,73 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
738
1029
|
if (!admitted) {
|
|
739
1030
|
throw new ComputerSessionStateError("ComputerSession controller authority changed");
|
|
740
1031
|
}
|
|
1032
|
+
const run = async (placement: ComputerPlacement): Promise<T> => {
|
|
1033
|
+
// The active controller binding is the provisioning fence. Live
|
|
1034
|
+
// input connects to it directly instead of re-ensuring the sidecar.
|
|
1035
|
+
const client = connectController(grant, record, placement);
|
|
1036
|
+
const tokens = deriveComputerSessionControllerTokens({
|
|
1037
|
+
rootSecret: controllerAuthorityRoot(deps),
|
|
1038
|
+
accountId: grant.accountId,
|
|
1039
|
+
workspaceId,
|
|
1040
|
+
placement: record.session.placement,
|
|
1041
|
+
placementInstanceId: placement.placementInstanceId,
|
|
1042
|
+
computerSessionId,
|
|
1043
|
+
controllerGeneration: binding.controllerGeneration,
|
|
1044
|
+
tokenGeneration: record.tokenGeneration,
|
|
1045
|
+
});
|
|
1046
|
+
const controller = {
|
|
1047
|
+
client,
|
|
1048
|
+
sessionClient: client.computerSessionClient({
|
|
1049
|
+
reference: {
|
|
1050
|
+
computerSessionId,
|
|
1051
|
+
controllerGeneration: binding.controllerGeneration,
|
|
1052
|
+
},
|
|
1053
|
+
...tokens,
|
|
1054
|
+
}),
|
|
1055
|
+
record,
|
|
1056
|
+
binding,
|
|
1057
|
+
placement,
|
|
1058
|
+
};
|
|
1059
|
+
let result: T;
|
|
1060
|
+
try {
|
|
1061
|
+
result = await callback(controller);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (!recoverMissing || !isMissingComputerControllerSession(error)) throw error;
|
|
1064
|
+
await client.createComputerSession({
|
|
1065
|
+
computerSessionId,
|
|
1066
|
+
controllerGeneration: binding.controllerGeneration,
|
|
1067
|
+
tokenGeneration: record.tokenGeneration,
|
|
1068
|
+
...tokens,
|
|
1069
|
+
});
|
|
1070
|
+
result = await callback(controller);
|
|
1071
|
+
}
|
|
1072
|
+
return result;
|
|
1073
|
+
};
|
|
1074
|
+
const cached = await cachedComputerPlacement(record);
|
|
1075
|
+
if (cached) {
|
|
1076
|
+
try {
|
|
1077
|
+
return await run(cached);
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
const safelyReplayable =
|
|
1080
|
+
channelOperation === "computer.read" || channelOperation === "computer.action";
|
|
1081
|
+
if (
|
|
1082
|
+
!safelyReplayable ||
|
|
1083
|
+
(!(error instanceof BrowserControlTransportError) &&
|
|
1084
|
+
!(error instanceof BrowserControlRequestError && error.retryable))
|
|
1085
|
+
) {
|
|
1086
|
+
throw error;
|
|
1087
|
+
}
|
|
1088
|
+
await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
1089
|
+
accountId: grant.accountId,
|
|
1090
|
+
workspaceId,
|
|
1091
|
+
sandboxGroupId: cached.lease!.sandboxGroupId,
|
|
1092
|
+
expectedEpoch: cached.lease!.leaseEpoch,
|
|
1093
|
+
expectedInstanceId: cached.placementInstanceId,
|
|
1094
|
+
controllerDataPlaneUrl: null,
|
|
1095
|
+
}).catch(() => null);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
741
1099
|
const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
742
1100
|
return await withComputerPlacement(
|
|
743
1101
|
sourceSession,
|
|
@@ -746,44 +1104,12 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
746
1104
|
binding.placementInstanceId,
|
|
747
1105
|
channelOperation,
|
|
748
1106
|
context.req.raw.signal,
|
|
749
|
-
async (placement) =>
|
|
750
|
-
|
|
751
|
-
grant,
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
);
|
|
756
|
-
const tokens = deriveComputerSessionControllerTokens({
|
|
757
|
-
rootSecret: controllerAuthorityRoot(deps),
|
|
758
|
-
accountId: grant.accountId,
|
|
759
|
-
workspaceId,
|
|
760
|
-
placement: record.session.placement,
|
|
761
|
-
placementInstanceId: placement.placementInstanceId,
|
|
762
|
-
computerSessionId,
|
|
763
|
-
controllerGeneration: binding.controllerGeneration,
|
|
764
|
-
tokenGeneration: record.tokenGeneration,
|
|
765
|
-
});
|
|
766
|
-
const result = await callback({
|
|
767
|
-
client,
|
|
768
|
-
sessionClient: client.computerSessionClient({
|
|
769
|
-
reference: {
|
|
770
|
-
computerSessionId,
|
|
771
|
-
controllerGeneration: binding.controllerGeneration,
|
|
772
|
-
},
|
|
773
|
-
...tokens,
|
|
774
|
-
}),
|
|
775
|
-
record,
|
|
776
|
-
binding,
|
|
777
|
-
placement,
|
|
778
|
-
});
|
|
779
|
-
await touchComputerSessionController(deps.db, {
|
|
780
|
-
accountId: grant.accountId,
|
|
781
|
-
workspaceId,
|
|
782
|
-
computerSessionId,
|
|
783
|
-
controllerGeneration: binding.controllerGeneration,
|
|
784
|
-
}).catch(() => false);
|
|
785
|
-
return result;
|
|
786
|
-
},
|
|
1107
|
+
async (placement) =>
|
|
1108
|
+
await run(
|
|
1109
|
+
await cacheComputerControllerPlacement(grant, workspaceId, placement).catch(
|
|
1110
|
+
() => placement,
|
|
1111
|
+
),
|
|
1112
|
+
),
|
|
787
1113
|
);
|
|
788
1114
|
} catch (error) {
|
|
789
1115
|
throw computerRouteError(error);
|
|
@@ -910,6 +1236,23 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
910
1236
|
).client;
|
|
911
1237
|
}
|
|
912
1238
|
|
|
1239
|
+
function connectController(
|
|
1240
|
+
grant: AccessGrant,
|
|
1241
|
+
record: ComputerSessionControlRecord,
|
|
1242
|
+
placement: ComputerPlacement,
|
|
1243
|
+
): BrowserControlClient {
|
|
1244
|
+
return new BrowserControlClient(placement.session, {
|
|
1245
|
+
adminToken: deriveBrowserControllerAdminToken({
|
|
1246
|
+
rootSecret: controllerAuthorityRoot(deps),
|
|
1247
|
+
accountId: grant.accountId,
|
|
1248
|
+
workspaceId: record.session.workspaceId,
|
|
1249
|
+
placement: record.session.placement,
|
|
1250
|
+
placementInstanceId: placement.placementInstanceId,
|
|
1251
|
+
}),
|
|
1252
|
+
nativeAuthority: nativeControllerAuthority(record.session.workspaceId, placement),
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
913
1256
|
async function releaseInteractionHolder(
|
|
914
1257
|
grant: AccessGrant,
|
|
915
1258
|
workspaceId: string,
|
|
@@ -1005,9 +1348,11 @@ function nativeControllerAuthority(
|
|
|
1005
1348
|
const placementId =
|
|
1006
1349
|
placement.placement.kind === "connected_machine"
|
|
1007
1350
|
? placement.placement.sandboxId
|
|
1008
|
-
: placement.placement.kind === "
|
|
1009
|
-
? placement.placement.
|
|
1010
|
-
:
|
|
1351
|
+
: placement.placement.kind === "attached_device"
|
|
1352
|
+
? placement.placement.deviceId
|
|
1353
|
+
: placement.placement.kind === "sandbox_group"
|
|
1354
|
+
? placement.placement.sandboxGroupId
|
|
1355
|
+
: null;
|
|
1011
1356
|
if (!placementId) {
|
|
1012
1357
|
throw new BrowserControlUnsupportedError("computer controller placement is unsupported");
|
|
1013
1358
|
}
|
|
@@ -1160,6 +1505,16 @@ function isTerminalOperation(state: string): boolean {
|
|
|
1160
1505
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
1161
1506
|
}
|
|
1162
1507
|
|
|
1508
|
+
function isMissingComputerControllerSession(error: unknown): boolean {
|
|
1509
|
+
if (!(error instanceof BrowserControlRequestError)) return false;
|
|
1510
|
+
if (error.status === 401 && error.error.code === "permission_denied") return true;
|
|
1511
|
+
return (
|
|
1512
|
+
error.status === 404 &&
|
|
1513
|
+
error.error.code === "resource_not_found" &&
|
|
1514
|
+
["computer session not found", "computer session is not active"].includes(error.error.message)
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1163
1518
|
function interactionFailure(error: unknown) {
|
|
1164
1519
|
if (error instanceof BrowserControlRequestError) return error.error;
|
|
1165
1520
|
if (
|