@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
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type SessionAuthorizationOperation,
|
|
26
26
|
} from "@opengeni/contracts";
|
|
27
27
|
import {
|
|
28
|
+
getSessionAuthorityEpoch,
|
|
28
29
|
acquireLease,
|
|
29
30
|
activateComputerSession,
|
|
30
31
|
completeComputerSessionEnd,
|
|
@@ -36,7 +37,8 @@ import {
|
|
|
36
37
|
findComputerSessionControlRecordByOperation,
|
|
37
38
|
getAttachedBrowserDevice,
|
|
38
39
|
getComputerSessionControlRecord,
|
|
39
|
-
|
|
40
|
+
getLiveEnrollmentConnection,
|
|
41
|
+
getSandbox,
|
|
40
42
|
getSession,
|
|
41
43
|
listComputerSessions,
|
|
42
44
|
prepareComputerSessionCreate,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
releaseLeaseHolder,
|
|
45
47
|
readLease,
|
|
46
48
|
recordLeaseControllerDataPlaneUrl,
|
|
49
|
+
terminalizeStaleConnectedInteractionPlacement,
|
|
47
50
|
touchComputerSessionController,
|
|
48
51
|
type ComputerSessionControlRecord,
|
|
49
52
|
type LeaseSnapshot,
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
requireAccessGrant,
|
|
53
56
|
requireSessionAuthorization,
|
|
54
57
|
relayConfigFromSettings,
|
|
58
|
+
resolveSessionSandboxRuntime,
|
|
55
59
|
SessionAuthorizationDeniedError,
|
|
56
60
|
SessionAuthorizationUnavailableError,
|
|
57
61
|
type ApiRouteDeps,
|
|
@@ -71,6 +75,7 @@ import {
|
|
|
71
75
|
NatsControlRpc,
|
|
72
76
|
NatsOpStreamTransport,
|
|
73
77
|
provisionBrowserControlClient,
|
|
78
|
+
renewSandboxProviderExpiration,
|
|
74
79
|
type BrowserControlPlacementSession,
|
|
75
80
|
} from "@opengeni/runtime/sandbox";
|
|
76
81
|
import type { Context, Hono } from "hono";
|
|
@@ -81,9 +86,21 @@ import {
|
|
|
81
86
|
deriveComputerSessionControllerTokens,
|
|
82
87
|
deriveComputerViewGrantToken,
|
|
83
88
|
} from "../browser-controller-authority";
|
|
84
|
-
import {
|
|
89
|
+
import { connectedMachineComputerAccessError } from "../connected-machine-computer-access";
|
|
90
|
+
import {
|
|
91
|
+
controllerCacheAllowsHostFetch,
|
|
92
|
+
controllerCachedUrlIsUsable,
|
|
93
|
+
shouldPersistControllerDataPlaneUrl,
|
|
94
|
+
withCachedController,
|
|
95
|
+
} from "../controller-data-plane";
|
|
85
96
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
86
|
-
import {
|
|
97
|
+
import { validateInteractionRequestOrigin } from "../http/cors";
|
|
98
|
+
import { ApiHttpError } from "../http/api-error";
|
|
99
|
+
import { interactionControlApiError } from "../http/interaction-control-error";
|
|
100
|
+
import {
|
|
101
|
+
createInteractionFrameProxyAttachment,
|
|
102
|
+
placementUsesInteractionFrameProxy,
|
|
103
|
+
} from "../interaction-frame-proxy";
|
|
87
104
|
import { observeComputerActionResult, observeLifecycleResult } from "../interaction-metrics";
|
|
88
105
|
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
89
106
|
|
|
@@ -141,7 +158,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
141
158
|
const request = await parseJsonBody(context, CreateComputerSessionRequest);
|
|
142
159
|
const startedAtMs = performance.now();
|
|
143
160
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
144
|
-
const origin = requestOrigin(context, deps.settings
|
|
161
|
+
const origin = requestOrigin(context, deps.settings);
|
|
145
162
|
const authority = controllerAuthorityRoot(deps);
|
|
146
163
|
|
|
147
164
|
try {
|
|
@@ -264,26 +281,27 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
264
281
|
() => placement,
|
|
265
282
|
);
|
|
266
283
|
} catch (error) {
|
|
267
|
-
|
|
284
|
+
const retryableRequestFailure =
|
|
285
|
+
error instanceof BrowserControlRequestError &&
|
|
286
|
+
error.retryable &&
|
|
287
|
+
error.error.code !== "machine_locked";
|
|
288
|
+
const outcomeUnknown =
|
|
289
|
+
error instanceof BrowserControlProtocolError ||
|
|
268
290
|
error instanceof BrowserControlTransportError ||
|
|
269
|
-
(error
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
throw error;
|
|
275
|
-
}
|
|
291
|
+
isAbort(error);
|
|
292
|
+
const rethrowAfterFailure =
|
|
293
|
+
error instanceof BrowserControlTransportError ||
|
|
294
|
+
retryableRequestFailure ||
|
|
295
|
+
isAbort(error);
|
|
276
296
|
const failed = await failComputerSessionOperation(deps.db, {
|
|
277
297
|
accountId: grant.accountId,
|
|
278
298
|
workspaceId,
|
|
279
299
|
operationId: request.operationId,
|
|
280
300
|
computerSessionId: preparedSession.id,
|
|
281
|
-
...(
|
|
282
|
-
? { state: "outcome_unknown" as const }
|
|
283
|
-
: {}),
|
|
301
|
+
...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
|
|
284
302
|
error: interactionFailure(error),
|
|
285
303
|
});
|
|
286
|
-
if (interactionHeld && !
|
|
304
|
+
if (interactionHeld && !outcomeUnknown) {
|
|
287
305
|
await releaseInteractionHolder(
|
|
288
306
|
grant,
|
|
289
307
|
workspaceId,
|
|
@@ -291,6 +309,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
291
309
|
placement.placement,
|
|
292
310
|
).catch(() => undefined);
|
|
293
311
|
}
|
|
312
|
+
if (rethrowAfterFailure) throw error;
|
|
294
313
|
return failed;
|
|
295
314
|
}
|
|
296
315
|
// These facts come from the physical adapter after its native helper
|
|
@@ -452,7 +471,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
452
471
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
|
|
453
472
|
async (context) => {
|
|
454
473
|
const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
|
|
455
|
-
const origin = requestOrigin(context, deps.settings
|
|
474
|
+
const origin = requestOrigin(context, deps.settings);
|
|
456
475
|
const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
|
|
457
476
|
const result = await withActiveComputerController(
|
|
458
477
|
context,
|
|
@@ -463,25 +482,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
463
482
|
"computer.attach",
|
|
464
483
|
async ({ client, sessionClient, record, binding, placement }) => {
|
|
465
484
|
if (origin) await client.addAllowedOrigins([origin]);
|
|
466
|
-
const grantId = randomUUID();
|
|
467
|
-
const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
|
|
468
|
-
const token = deriveComputerViewGrantToken({
|
|
469
|
-
rootSecret: controllerAuthorityRoot(deps),
|
|
470
|
-
accountId: grant.accountId,
|
|
471
|
-
workspaceId,
|
|
472
|
-
placement: record.session.placement,
|
|
473
|
-
placementInstanceId: placement.placementInstanceId,
|
|
474
|
-
computerSessionId,
|
|
475
|
-
controllerGeneration: binding.controllerGeneration,
|
|
476
|
-
tokenGeneration: record.tokenGeneration,
|
|
477
|
-
grantId,
|
|
478
|
-
expiresAt,
|
|
479
|
-
});
|
|
480
485
|
const reference = {
|
|
481
486
|
computerSessionId,
|
|
482
487
|
controllerGeneration: binding.controllerGeneration,
|
|
483
488
|
};
|
|
484
|
-
await client.createComputerViewGrant(reference, { grantId, token, expiresAt });
|
|
485
489
|
const target = (await sessionClient.listTargets()).find(
|
|
486
490
|
(candidate) => candidate.id === request.targetId,
|
|
487
491
|
);
|
|
@@ -492,6 +496,33 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
492
496
|
retryable: false,
|
|
493
497
|
});
|
|
494
498
|
}
|
|
499
|
+
if (target.kind === "app") {
|
|
500
|
+
throw new BrowserControlRequestError(409, {
|
|
501
|
+
code: "unsupported",
|
|
502
|
+
message:
|
|
503
|
+
"application targets support semantic control but do not provide a visual stream; select a window or screen",
|
|
504
|
+
retryable: false,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const grantId = randomUUID();
|
|
508
|
+
const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
|
|
509
|
+
const token = deriveComputerViewGrantToken({
|
|
510
|
+
rootSecret: controllerAuthorityRoot(deps),
|
|
511
|
+
accountId: grant.accountId,
|
|
512
|
+
workspaceId,
|
|
513
|
+
placement: record.session.placement,
|
|
514
|
+
placementInstanceId: placement.placementInstanceId,
|
|
515
|
+
computerSessionId,
|
|
516
|
+
controllerGeneration: binding.controllerGeneration,
|
|
517
|
+
tokenGeneration: record.tokenGeneration,
|
|
518
|
+
grantId,
|
|
519
|
+
expiresAt,
|
|
520
|
+
});
|
|
521
|
+
await client.createComputerViewGrant(reference, {
|
|
522
|
+
grantId,
|
|
523
|
+
token,
|
|
524
|
+
expiresAt,
|
|
525
|
+
});
|
|
495
526
|
const relaySecret = placement.session.openComputerFrames
|
|
496
527
|
? resolveStreamTokenSecret(deps.settings)
|
|
497
528
|
: null;
|
|
@@ -510,15 +541,31 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
510
541
|
...(request.stream ? { stream: request.stream } : {}),
|
|
511
542
|
});
|
|
512
543
|
} catch (error) {
|
|
544
|
+
const publicFailure = interactionControlApiError(error, "computer");
|
|
513
545
|
console.error("computer frame relay open failed", {
|
|
514
546
|
computerSessionId,
|
|
515
547
|
targetId: request.targetId,
|
|
516
|
-
|
|
548
|
+
failureCode: publicFailure?.details?.controlFailureCode ?? "unclassified",
|
|
549
|
+
retryable: publicFailure?.retryable ?? false,
|
|
550
|
+
outcomeUnknown: publicFailure?.outcomeUnknown ?? false,
|
|
551
|
+
controlRequestId: publicFailure?.details?.controlRequestId ?? null,
|
|
517
552
|
});
|
|
518
553
|
throw error;
|
|
519
554
|
}
|
|
520
555
|
const stream = relayed
|
|
521
556
|
? await (async () => {
|
|
557
|
+
// 0281: stamp the authenticated viewer subject and the live
|
|
558
|
+
// session authority epoch into the relay stream token.
|
|
559
|
+
const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
|
|
560
|
+
accountId: grant.accountId,
|
|
561
|
+
workspaceId,
|
|
562
|
+
sessionId: record.sourceSessionId,
|
|
563
|
+
});
|
|
564
|
+
if (relayAuthorityEpoch === null) {
|
|
565
|
+
throw new BrowserControlUnsupportedError(
|
|
566
|
+
"stream authority is unavailable for this session",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
522
569
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
523
570
|
workspaceId,
|
|
524
571
|
sessionId: record.sourceSessionId,
|
|
@@ -526,6 +573,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
526
573
|
leaseEpoch: record.tokenGeneration,
|
|
527
574
|
port: relayed.channel.port,
|
|
528
575
|
ttlSeconds: request.expiresInSeconds,
|
|
576
|
+
subjectId: grant.subjectId,
|
|
577
|
+
authorityEpoch: relayAuthorityEpoch,
|
|
529
578
|
});
|
|
530
579
|
return {
|
|
531
580
|
kind: "relay" as const,
|
|
@@ -540,30 +589,55 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
540
589
|
},
|
|
541
590
|
};
|
|
542
591
|
})()
|
|
543
|
-
:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
kind
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
592
|
+
: await (async () => {
|
|
593
|
+
const rfb =
|
|
594
|
+
record.session.placement.kind === "sandbox_group" &&
|
|
595
|
+
record.session.platform === "linux" &&
|
|
596
|
+
target.kind === "screen";
|
|
597
|
+
const protocols = rfb
|
|
598
|
+
? [
|
|
599
|
+
"binary",
|
|
600
|
+
COMPUTER_RFB_WEBSOCKET_PROTOCOL,
|
|
601
|
+
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
602
|
+
]
|
|
603
|
+
: [
|
|
604
|
+
COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
605
|
+
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
606
|
+
];
|
|
607
|
+
const upstreamUrl = rfb
|
|
608
|
+
? await client.computerRfbStreamUrl(reference, request.targetId)
|
|
609
|
+
: await client.computerFrameStreamUrl(
|
|
610
|
+
reference,
|
|
611
|
+
request.targetId,
|
|
612
|
+
request.stream,
|
|
613
|
+
);
|
|
614
|
+
const attachment = placementUsesInteractionFrameProxy(placement.lease?.backend, {
|
|
615
|
+
openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
616
|
+
...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
|
|
617
|
+
? {
|
|
618
|
+
openSandboxInteractionFrameProxy:
|
|
619
|
+
deps.settings.openSandboxInteractionFrameProxy,
|
|
620
|
+
}
|
|
621
|
+
: {}),
|
|
622
|
+
})
|
|
623
|
+
? createInteractionFrameProxyAttachment({
|
|
624
|
+
requestUrl: context.req.url,
|
|
625
|
+
publicBaseUrl: deps.settings.publicBaseUrl,
|
|
626
|
+
webBaseUrl: deps.settings.webBaseUrl,
|
|
627
|
+
forwardedProto: context.req.header("x-forwarded-proto"),
|
|
628
|
+
forwardedHost:
|
|
629
|
+
context.req.header("x-forwarded-host") ?? context.req.header("host"),
|
|
630
|
+
rootSecret: controllerAuthorityRoot(deps),
|
|
631
|
+
upstreamUrl,
|
|
632
|
+
upstreamProtocols: protocols,
|
|
633
|
+
origin,
|
|
634
|
+
expiresAt,
|
|
635
|
+
})
|
|
636
|
+
: { url: upstreamUrl, protocols };
|
|
637
|
+
return rfb
|
|
638
|
+
? { kind: "direct_rfb" as const, ...attachment }
|
|
639
|
+
: { kind: "direct_websocket" as const, ...attachment };
|
|
640
|
+
})();
|
|
567
641
|
return ComputerSessionAttachment.parse({
|
|
568
642
|
computerSessionId,
|
|
569
643
|
controllerGeneration: binding.controllerGeneration,
|
|
@@ -614,7 +688,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
614
688
|
const computerSessionId = requireUuidParam(context, "computerSessionId");
|
|
615
689
|
const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
|
|
616
690
|
const startedAtMs = performance.now();
|
|
617
|
-
const origin = requestOrigin(context, deps.settings
|
|
691
|
+
const origin = requestOrigin(context, deps.settings);
|
|
618
692
|
try {
|
|
619
693
|
const before = await getComputerSessionControlRecord(deps.db, {
|
|
620
694
|
accountId: grant.accountId,
|
|
@@ -765,23 +839,41 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
765
839
|
if (device.state !== "connected") {
|
|
766
840
|
throw new ComputerSessionStateError("Attached browser machine is disconnected");
|
|
767
841
|
}
|
|
768
|
-
const enrollment = await
|
|
842
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
769
843
|
deps.db,
|
|
770
844
|
sourceSession.workspaceId,
|
|
771
845
|
device.enrollmentId,
|
|
772
846
|
);
|
|
773
|
-
if (!enrollment || enrollment.status !== "active") {
|
|
847
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
774
848
|
throw new ComputerSessionStateError("Attached browser machine is unavailable");
|
|
775
849
|
}
|
|
776
|
-
|
|
850
|
+
if (!enrollment.workspaceRoot) {
|
|
851
|
+
throw new ComputerSessionStateError(
|
|
852
|
+
"Attached browser machine has not reported an absolute workspace root",
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
if (operation !== "computer.end") {
|
|
856
|
+
assertConnectedMachineComputerAccess(enrollment, operation);
|
|
857
|
+
}
|
|
858
|
+
const placementInstanceId = attachedEndPlacementInstanceId(
|
|
859
|
+
operation,
|
|
860
|
+
expectedPlacementInstanceId,
|
|
861
|
+
device.connectionGeneration,
|
|
862
|
+
);
|
|
777
863
|
const built = await buildSelfhostedBackendSession({
|
|
778
864
|
workspaceId: sourceSession.workspaceId,
|
|
779
865
|
agentId: device.enrollmentId,
|
|
866
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
867
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
780
868
|
relay: relayConfigFromSettings(deps.settings),
|
|
781
869
|
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
782
870
|
epoch: 0,
|
|
783
871
|
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
784
872
|
execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
|
|
873
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
874
|
+
operationResourcePolicySupported:
|
|
875
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
876
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
785
877
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
786
878
|
enrollment.opStream === true &&
|
|
787
879
|
deps.bus.getOpStreamConnection
|
|
@@ -797,7 +889,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
797
889
|
waitSignal.throwIfAborted();
|
|
798
890
|
return await callback({
|
|
799
891
|
placement: expectedPlacement,
|
|
800
|
-
placementInstanceId
|
|
892
|
+
placementInstanceId,
|
|
801
893
|
session: built.session as unknown as BrowserControlPlacementSession,
|
|
802
894
|
lease: null,
|
|
803
895
|
});
|
|
@@ -838,13 +930,30 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
838
930
|
}
|
|
839
931
|
|
|
840
932
|
const resolved = await handle.routingSession.prime();
|
|
933
|
+
if (operation !== "computer.end" && resolved.kind === "selfhosted" && resolved.sandboxId) {
|
|
934
|
+
const sandbox = await getSandbox(deps.db, grant, resolved.sandboxId);
|
|
935
|
+
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
936
|
+
throw new ComputerSessionStateError("Connected Machine placement is unavailable");
|
|
937
|
+
}
|
|
938
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
939
|
+
deps.db,
|
|
940
|
+
grant,
|
|
941
|
+
sandbox.enrollmentId,
|
|
942
|
+
);
|
|
943
|
+
if (!enrollment?.connectionInstanceId) {
|
|
944
|
+
throw new ComputerSessionStateError("Connected Machine is unavailable");
|
|
945
|
+
}
|
|
946
|
+
assertConnectedMachineComputerAccess(enrollment, operation);
|
|
947
|
+
}
|
|
841
948
|
if (expectedPlacement?.kind === "connected_machine") {
|
|
842
949
|
if (
|
|
843
950
|
resolved.kind !== "selfhosted" ||
|
|
844
951
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
845
952
|
) {
|
|
846
|
-
|
|
847
|
-
|
|
953
|
+
return await throwComputerSourcePlacementChanged(
|
|
954
|
+
grant,
|
|
955
|
+
sourceSession.id,
|
|
956
|
+
expectedPlacement.sandboxId,
|
|
848
957
|
);
|
|
849
958
|
}
|
|
850
959
|
const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
|
|
@@ -878,7 +987,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
878
987
|
}
|
|
879
988
|
if (resolved.kind === "selfhosted") {
|
|
880
989
|
return await callback({
|
|
881
|
-
placement: {
|
|
990
|
+
placement: {
|
|
991
|
+
kind: "connected_machine",
|
|
992
|
+
sandboxId: resolved.sandboxId,
|
|
993
|
+
},
|
|
882
994
|
placementInstanceId: resolved.providerInstanceId ?? resolved.sandboxId,
|
|
883
995
|
session: resolved.session as unknown as BrowserControlPlacementSession,
|
|
884
996
|
lease: null,
|
|
@@ -914,7 +1026,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
914
1026
|
!lease ||
|
|
915
1027
|
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
916
1028
|
lease.instanceId !== binding.placementInstanceId ||
|
|
917
|
-
!lease.controllerDataPlaneUrl
|
|
1029
|
+
!lease.controllerDataPlaneUrl ||
|
|
1030
|
+
(lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
|
|
1031
|
+
!controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
|
|
918
1032
|
) {
|
|
919
1033
|
return null;
|
|
920
1034
|
}
|
|
@@ -938,8 +1052,29 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
938
1052
|
) {
|
|
939
1053
|
return placement;
|
|
940
1054
|
}
|
|
1055
|
+
if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
|
|
1056
|
+
if (!placement.lease.controllerDataPlaneUrl) return placement;
|
|
1057
|
+
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
1058
|
+
accountId: grant.accountId,
|
|
1059
|
+
workspaceId,
|
|
1060
|
+
sandboxGroupId: placement.placement.sandboxGroupId,
|
|
1061
|
+
expectedEpoch: placement.lease.leaseEpoch,
|
|
1062
|
+
expectedInstanceId: placement.lease.instanceId,
|
|
1063
|
+
controllerDataPlaneUrl: null,
|
|
1064
|
+
});
|
|
1065
|
+
return lease ? { ...placement, lease } : placement;
|
|
1066
|
+
}
|
|
941
1067
|
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
942
1068
|
const url = buildStreamUrl(endpoint);
|
|
1069
|
+
if (
|
|
1070
|
+
!shouldPersistControllerDataPlaneUrl({
|
|
1071
|
+
backend: placement.lease.backend,
|
|
1072
|
+
signedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
1073
|
+
url,
|
|
1074
|
+
})
|
|
1075
|
+
) {
|
|
1076
|
+
return placement;
|
|
1077
|
+
}
|
|
943
1078
|
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
944
1079
|
accountId: grant.accountId,
|
|
945
1080
|
workspaceId,
|
|
@@ -949,6 +1084,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
949
1084
|
controllerDataPlaneUrl: url,
|
|
950
1085
|
});
|
|
951
1086
|
if (!lease) return placement;
|
|
1087
|
+
if (!controllerCacheAllowsHostFetch(url)) {
|
|
1088
|
+
return { ...placement, lease };
|
|
1089
|
+
}
|
|
952
1090
|
return { ...placement, session: controllerOnlySession(url), lease };
|
|
953
1091
|
}
|
|
954
1092
|
|
|
@@ -964,7 +1102,13 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
964
1102
|
const sandboxGroupId =
|
|
965
1103
|
placement.placement.kind === "sandbox_group" ? placement.placement.sandboxGroupId : null;
|
|
966
1104
|
return await withCachedController({
|
|
967
|
-
cachedUrl:
|
|
1105
|
+
cachedUrl:
|
|
1106
|
+
sandboxGroupId &&
|
|
1107
|
+
cachedUrl &&
|
|
1108
|
+
!(placement.lease?.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) &&
|
|
1109
|
+
controllerCachedUrlIsUsable(cachedUrl)
|
|
1110
|
+
? cachedUrl
|
|
1111
|
+
: null,
|
|
968
1112
|
createCachedClient: (url) =>
|
|
969
1113
|
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
970
1114
|
prepareCachedClient: async (client) => {
|
|
@@ -1016,6 +1160,17 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1016
1160
|
computerSessionId,
|
|
1017
1161
|
});
|
|
1018
1162
|
await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
|
|
1163
|
+
let sourceSession: Session | null = null;
|
|
1164
|
+
if (record.session.placement?.kind === "connected_machine") {
|
|
1165
|
+
sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
1166
|
+
if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
|
|
1167
|
+
return await throwComputerSourcePlacementChanged(
|
|
1168
|
+
grant,
|
|
1169
|
+
record.sourceSessionId,
|
|
1170
|
+
record.session.placement.sandboxId,
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1019
1174
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
1020
1175
|
throw new ComputerSessionStateError("ComputerSession is not active");
|
|
1021
1176
|
}
|
|
@@ -1096,7 +1251,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1096
1251
|
}
|
|
1097
1252
|
}
|
|
1098
1253
|
|
|
1099
|
-
|
|
1254
|
+
sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
1100
1255
|
return await withComputerPlacement(
|
|
1101
1256
|
sourceSession,
|
|
1102
1257
|
grant,
|
|
@@ -1116,6 +1271,25 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1116
1271
|
}
|
|
1117
1272
|
}
|
|
1118
1273
|
|
|
1274
|
+
async function throwComputerSourcePlacementChanged(
|
|
1275
|
+
grant: AccessGrant,
|
|
1276
|
+
sourceSessionId: string,
|
|
1277
|
+
connectedSandboxId: string,
|
|
1278
|
+
): Promise<never> {
|
|
1279
|
+
const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
|
|
1280
|
+
accountId: grant.accountId,
|
|
1281
|
+
workspaceId: grant.workspaceId,
|
|
1282
|
+
sourceSessionId,
|
|
1283
|
+
connectedSandboxId,
|
|
1284
|
+
});
|
|
1285
|
+
if (reconciliation.sourcePlacementChanged) {
|
|
1286
|
+
throw sourcePlacementChangedApiError("computer_session");
|
|
1287
|
+
}
|
|
1288
|
+
throw new ComputerSessionStateError(
|
|
1289
|
+
"ComputerSession source placement changed during the request; retry",
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1119
1293
|
async function ensureInteractionHolder(
|
|
1120
1294
|
grant: AccessGrant,
|
|
1121
1295
|
sourceSession: Session,
|
|
@@ -1127,6 +1301,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1127
1301
|
if (!placement.lease?.instanceId) {
|
|
1128
1302
|
throw new ComputerSessionStateError("ComputerSession lease placement is unavailable");
|
|
1129
1303
|
}
|
|
1304
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(
|
|
1305
|
+
deps.db,
|
|
1306
|
+
deps.settings,
|
|
1307
|
+
sourceSession,
|
|
1308
|
+
);
|
|
1130
1309
|
const acquired = await acquireLease(deps.db, {
|
|
1131
1310
|
accountId: grant.accountId,
|
|
1132
1311
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -1136,8 +1315,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1136
1315
|
subjectId: sourceSession.id,
|
|
1137
1316
|
backend: placement.lease.backend,
|
|
1138
1317
|
os: placement.lease.os,
|
|
1139
|
-
image:
|
|
1140
|
-
rigVersionId:
|
|
1318
|
+
image: sandboxRuntime.image,
|
|
1319
|
+
rigVersionId: sourceSession.rigVersionId,
|
|
1141
1320
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
1142
1321
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
1143
1322
|
waitSignal,
|
|
@@ -1158,6 +1337,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1158
1337
|
).catch(() => undefined);
|
|
1159
1338
|
throw new ComputerSessionStateError("ComputerSession placement fence changed; retry");
|
|
1160
1339
|
}
|
|
1340
|
+
await renewSandboxProviderExpiration({
|
|
1341
|
+
backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
|
|
1342
|
+
settings: deps.settings,
|
|
1343
|
+
instanceId: acquired.lease.instanceId,
|
|
1344
|
+
}).catch(() => false);
|
|
1161
1345
|
return true;
|
|
1162
1346
|
}
|
|
1163
1347
|
|
|
@@ -1273,7 +1457,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1273
1457
|
async function routePreamble(
|
|
1274
1458
|
context: Context,
|
|
1275
1459
|
permission: "sessions:read" | "sessions:control" | "stream:view",
|
|
1276
|
-
): Promise<{
|
|
1460
|
+
): Promise<{
|
|
1461
|
+
workspaceId: string;
|
|
1462
|
+
grant: AccessGrant;
|
|
1463
|
+
computerSessionId: string;
|
|
1464
|
+
}> {
|
|
1277
1465
|
const workspaceId = context.req.param("workspaceId") ?? "";
|
|
1278
1466
|
const grant = await requireAccessGrant(context, deps, workspaceId, permission);
|
|
1279
1467
|
return {
|
|
@@ -1284,6 +1472,21 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1284
1472
|
}
|
|
1285
1473
|
}
|
|
1286
1474
|
|
|
1475
|
+
function sourcePlacementChangedApiError(interactionResource: "computer_session"): ApiHttpError {
|
|
1476
|
+
return new ApiHttpError(409, {
|
|
1477
|
+
code: "conflict",
|
|
1478
|
+
message:
|
|
1479
|
+
"This Desktop belonged to a previous task placement and was retired. Open a new Desktop.",
|
|
1480
|
+
retryable: false,
|
|
1481
|
+
outcomeUnknown: false,
|
|
1482
|
+
details: {
|
|
1483
|
+
interactionResource,
|
|
1484
|
+
interactionFailureCode: "source_placement_changed",
|
|
1485
|
+
interactionLifecycle: "lost",
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1287
1490
|
function computerCreateInput(
|
|
1288
1491
|
grant: AccessGrant,
|
|
1289
1492
|
workspaceId: string,
|
|
@@ -1400,7 +1603,10 @@ async function authorizeSourceSession(
|
|
|
1400
1603
|
});
|
|
1401
1604
|
} catch (error) {
|
|
1402
1605
|
if (error instanceof SessionAuthorizationDeniedError) {
|
|
1403
|
-
throw new HTTPException(404, {
|
|
1606
|
+
throw new HTTPException(404, {
|
|
1607
|
+
message: "session not found",
|
|
1608
|
+
cause: error,
|
|
1609
|
+
});
|
|
1404
1610
|
}
|
|
1405
1611
|
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
1406
1612
|
throw new HTTPException(503, {
|
|
@@ -1456,26 +1662,8 @@ function requireOpaqueParam(context: Context, name: string): string {
|
|
|
1456
1662
|
return value;
|
|
1457
1663
|
}
|
|
1458
1664
|
|
|
1459
|
-
function requestOrigin(context: Context,
|
|
1460
|
-
|
|
1461
|
-
if (!value) return null;
|
|
1462
|
-
let url: URL;
|
|
1463
|
-
try {
|
|
1464
|
-
url = new URL(value);
|
|
1465
|
-
} catch {
|
|
1466
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
1467
|
-
}
|
|
1468
|
-
if (
|
|
1469
|
-
url.origin === "null" ||
|
|
1470
|
-
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
1471
|
-
url.origin !== value
|
|
1472
|
-
) {
|
|
1473
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
1474
|
-
}
|
|
1475
|
-
if (!allowedCorsOrigin(allowedPattern, url.origin)) {
|
|
1476
|
-
throw new HTTPException(403, { message: "request origin is not allowed" });
|
|
1477
|
-
}
|
|
1478
|
-
return url.origin;
|
|
1665
|
+
function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
|
|
1666
|
+
return validateInteractionRequestOrigin(context.req.header("origin"), settings);
|
|
1479
1667
|
}
|
|
1480
1668
|
|
|
1481
1669
|
function interactionActorForGrant(grant: AccessGrant): ReturnType<typeof InteractionActor.parse> {
|
|
@@ -1501,6 +1689,39 @@ function assertPlacementInstance(expected: string | null, actual: string): void
|
|
|
1501
1689
|
}
|
|
1502
1690
|
}
|
|
1503
1691
|
|
|
1692
|
+
/** End must still reach the live agent with the session's original token
|
|
1693
|
+
* fence. A later Chrome generation must not block ScreenCaptureKit teardown. */
|
|
1694
|
+
function attachedEndPlacementInstanceId(
|
|
1695
|
+
operation: ChannelAOperation,
|
|
1696
|
+
expectedPlacementInstanceId: string | null,
|
|
1697
|
+
liveGeneration: string,
|
|
1698
|
+
): string {
|
|
1699
|
+
if (operation === "computer.end" && expectedPlacementInstanceId) {
|
|
1700
|
+
return expectedPlacementInstanceId;
|
|
1701
|
+
}
|
|
1702
|
+
assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
|
|
1703
|
+
return liveGeneration;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function assertConnectedMachineComputerAccess(
|
|
1707
|
+
enrollment: {
|
|
1708
|
+
hasDisplay: boolean;
|
|
1709
|
+
desktopUnavailableReason: string | null;
|
|
1710
|
+
allowScreenControl: boolean;
|
|
1711
|
+
},
|
|
1712
|
+
operation: ChannelAOperation,
|
|
1713
|
+
): void {
|
|
1714
|
+
const error = connectedMachineComputerAccessError(
|
|
1715
|
+
enrollment,
|
|
1716
|
+
operation === "computer.action" || operation === "computer.control",
|
|
1717
|
+
);
|
|
1718
|
+
if (!error) return;
|
|
1719
|
+
if (error.status === 403) {
|
|
1720
|
+
throw new HTTPException(403, { message: error.message });
|
|
1721
|
+
}
|
|
1722
|
+
throw new ComputerSessionStateError(error.message);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1504
1725
|
function isTerminalOperation(state: string): boolean {
|
|
1505
1726
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
1506
1727
|
}
|
|
@@ -1521,7 +1742,11 @@ function interactionFailure(error: unknown) {
|
|
|
1521
1742
|
error instanceof BrowserControlUnsupportedError ||
|
|
1522
1743
|
error instanceof BrowserControlServerUnsupportedError
|
|
1523
1744
|
) {
|
|
1524
|
-
return {
|
|
1745
|
+
return {
|
|
1746
|
+
code: "unsupported" as const,
|
|
1747
|
+
message: error.message,
|
|
1748
|
+
retryable: false,
|
|
1749
|
+
};
|
|
1525
1750
|
}
|
|
1526
1751
|
if (error instanceof BrowserControlServerError) {
|
|
1527
1752
|
return {
|
|
@@ -1541,6 +1766,8 @@ function interactionFailure(error: unknown) {
|
|
|
1541
1766
|
}
|
|
1542
1767
|
|
|
1543
1768
|
function computerRouteError(error: unknown): HTTPException {
|
|
1769
|
+
const connectedMachineError = interactionControlApiError(error, "computer");
|
|
1770
|
+
if (connectedMachineError) return connectedMachineError;
|
|
1544
1771
|
if (error instanceof HTTPException) return error;
|
|
1545
1772
|
if (error instanceof ComputerSessionNotFoundError) {
|
|
1546
1773
|
return new HTTPException(404, { message: error.message, cause: error });
|