@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
package/src/sandbox/viewer.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { githubAppBotIdentity } from "@opengeni/github";
|
|
|
33
33
|
import { type Session, type StreamUrlRotatedPayload } from "@opengeni/contracts";
|
|
34
34
|
import {
|
|
35
35
|
acquireLease,
|
|
36
|
+
getLiveEnrollmentConnection,
|
|
36
37
|
getSandbox,
|
|
37
38
|
getSandboxSessionEnvelope,
|
|
38
39
|
heartbeatLeaseHolder,
|
|
@@ -48,6 +49,8 @@ import {
|
|
|
48
49
|
type Database,
|
|
49
50
|
type LeaseSnapshot,
|
|
50
51
|
type SandboxRecord,
|
|
52
|
+
getSessionAuthorityEpoch,
|
|
53
|
+
namedSubjectHasLiveWorkspaceAuthority,
|
|
51
54
|
} from "@opengeni/db";
|
|
52
55
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
53
56
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -71,11 +74,16 @@ import {
|
|
|
71
74
|
TerminalServerUnsupportedError,
|
|
72
75
|
verifySandboxExecReadiness,
|
|
73
76
|
StreamPortUnavailableError,
|
|
77
|
+
renewSandboxProviderExpiration,
|
|
74
78
|
type ControlRpc,
|
|
75
79
|
type EstablishedSandboxSession,
|
|
76
80
|
type NatsRequestConnection,
|
|
77
81
|
} from "@opengeni/runtime/sandbox";
|
|
78
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
providerSettingsForSessionSandboxRuntime,
|
|
84
|
+
relayConfigFromSettings,
|
|
85
|
+
resolveSessionSandboxRuntime,
|
|
86
|
+
} from "@opengeni/core";
|
|
79
87
|
import { establishApiSandboxSpawner } from "./rematerialize";
|
|
80
88
|
import { establishCachedChannelAHandle } from "./channel-a";
|
|
81
89
|
|
|
@@ -137,12 +145,19 @@ export async function sessionAttachEnvironment(
|
|
|
137
145
|
services: ViewerServices,
|
|
138
146
|
workspaceId: string,
|
|
139
147
|
session: Session,
|
|
148
|
+
/** The authenticated route subject driving this attach (0282); recorded on
|
|
149
|
+
* the materialization audit fact. Null records the legacy service sentinel. */
|
|
150
|
+
attachSubjectId: string | null,
|
|
140
151
|
): Promise<Record<string, string>> {
|
|
141
152
|
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
|
|
142
153
|
services.db,
|
|
143
154
|
services.settings,
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
{
|
|
156
|
+
accountId: session.accountId,
|
|
157
|
+
workspaceId,
|
|
158
|
+
variableSetId: session.environmentId,
|
|
159
|
+
authority: { kind: "session_attach", sessionId: session.id, subjectId: attachSubjectId },
|
|
160
|
+
},
|
|
146
161
|
);
|
|
147
162
|
// Build the env with the SESSION's backend, not the deployment default: the
|
|
148
163
|
// stable base is backend-aware (HOME = the descriptor workspaceRoot, and the
|
|
@@ -187,6 +202,9 @@ export async function attachViewer(
|
|
|
187
202
|
workspaceId: string;
|
|
188
203
|
session: Session;
|
|
189
204
|
viewerId?: string;
|
|
205
|
+
/** The authenticated subject attaching this viewer (0281): recorded on
|
|
206
|
+
* the holder row together with the live session authority epoch. */
|
|
207
|
+
viewerSubjectId?: string;
|
|
190
208
|
/** Cancel lifecycle waiting when the originating HTTP request disconnects. */
|
|
191
209
|
waitSignal?: AbortSignal;
|
|
192
210
|
},
|
|
@@ -194,8 +212,13 @@ export async function attachViewer(
|
|
|
194
212
|
const { db, settings } = services;
|
|
195
213
|
const { accountId, workspaceId, session } = input;
|
|
196
214
|
const viewerId = input.viewerId ?? crypto.randomUUID();
|
|
215
|
+
const attachSubjectId = claimableSubjectId(input.viewerSubjectId ?? null);
|
|
216
|
+
const attachAuthorityEpoch = attachSubjectId
|
|
217
|
+
? await getSessionAuthorityEpoch(db, { accountId, workspaceId, sessionId: session.id })
|
|
218
|
+
: null;
|
|
197
219
|
const leaseTtlMs = settings.sandboxLeaseTtlMs;
|
|
198
220
|
const sandboxGroupId = session.sandboxGroupId;
|
|
221
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(db, settings, session);
|
|
199
222
|
|
|
200
223
|
const release = async (): Promise<void> => {
|
|
201
224
|
await releaseLeaseHolder(db, {
|
|
@@ -217,8 +240,12 @@ export async function attachViewer(
|
|
|
217
240
|
kind: "viewer",
|
|
218
241
|
holderId: viewerId,
|
|
219
242
|
subjectId: session.id,
|
|
243
|
+
...(attachSubjectId ? { viewerSubjectId: attachSubjectId } : {}),
|
|
244
|
+
...(attachAuthorityEpoch !== null ? { viewerAuthorityEpoch: attachAuthorityEpoch } : {}),
|
|
220
245
|
backend: session.sandboxBackend,
|
|
221
246
|
os: session.sandboxOs,
|
|
247
|
+
image: sandboxRuntime.image,
|
|
248
|
+
rigVersionId: session.rigVersionId,
|
|
222
249
|
leaseTtlMs,
|
|
223
250
|
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
224
251
|
captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
|
|
@@ -270,7 +297,16 @@ export async function attachViewer(
|
|
|
270
297
|
// so the next turn's agent-manifest apply finds an EMPTY environment delta in
|
|
271
298
|
// the SDK's validateNoEnvironmentDelta (otherwise: "Live sandbox sessions
|
|
272
299
|
// cannot change manifest environment variables").
|
|
273
|
-
const environment = await sessionAttachEnvironment(
|
|
300
|
+
const environment = await sessionAttachEnvironment(
|
|
301
|
+
services,
|
|
302
|
+
workspaceId,
|
|
303
|
+
session,
|
|
304
|
+
input.viewerSubjectId ?? null,
|
|
305
|
+
);
|
|
306
|
+
const providerSettings = await providerSettingsForSessionSandboxRuntime(
|
|
307
|
+
sandboxRuntime,
|
|
308
|
+
session.sandboxBackend,
|
|
309
|
+
);
|
|
274
310
|
// Prefer the COLD lease's preserved resume_state when it carries a persisted
|
|
275
311
|
// /workspace snapshot (confirmDrainCold keeps a minimal archive-only envelope
|
|
276
312
|
// across draining->cold). establishSandboxSessionFromEnvelope cold-creates a
|
|
@@ -279,7 +315,7 @@ export async function attachViewer(
|
|
|
279
315
|
// session envelope (a never-warmed cold start).
|
|
280
316
|
const result = await establishApiSandboxSpawner({
|
|
281
317
|
db,
|
|
282
|
-
settings,
|
|
318
|
+
settings: providerSettings,
|
|
283
319
|
accountId,
|
|
284
320
|
workspaceId,
|
|
285
321
|
sandboxGroupId,
|
|
@@ -353,11 +389,16 @@ export async function attachViewer(
|
|
|
353
389
|
message: `sandbox is ${live.recovery.restore.status} at epoch ${live.leaseEpoch}`,
|
|
354
390
|
});
|
|
355
391
|
}
|
|
356
|
-
const environment = await sessionAttachEnvironment(
|
|
392
|
+
const environment = await sessionAttachEnvironment(
|
|
393
|
+
services,
|
|
394
|
+
workspaceId,
|
|
395
|
+
session,
|
|
396
|
+
input.viewerSubjectId ?? null,
|
|
397
|
+
);
|
|
357
398
|
let observed: EstablishedSandboxSession | undefined;
|
|
358
399
|
try {
|
|
359
400
|
const establish = services.establishSandboxSession ?? establishSandboxSessionFromEnvelope;
|
|
360
|
-
observed = await establish(settings, live.resumeState, {
|
|
401
|
+
observed = await establish(sandboxRuntime.settings, live.resumeState, {
|
|
361
402
|
sessionId: session.id,
|
|
362
403
|
recovery: "resume-only",
|
|
363
404
|
backendOverride: session.sandboxBackend,
|
|
@@ -430,13 +471,26 @@ export type SessionGroupReadinessHold = {
|
|
|
430
471
|
* it. */
|
|
431
472
|
export async function ensureSessionGroupReady(
|
|
432
473
|
services: ViewerServices,
|
|
433
|
-
input: {
|
|
474
|
+
input: {
|
|
475
|
+
accountId: string;
|
|
476
|
+
workspaceId: string;
|
|
477
|
+
session: Session;
|
|
478
|
+
/** The authenticated subject driving the fleet attach/swap (0282): recorded
|
|
479
|
+
* on the viewer holder and the materialization audit fact. Omitting it
|
|
480
|
+
* records the explicit service sentinel, so forward the route subject. */
|
|
481
|
+
subjectId?: string | null;
|
|
482
|
+
},
|
|
434
483
|
): Promise<SessionGroupReadinessHold> {
|
|
435
484
|
let lastError: unknown;
|
|
436
485
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
437
486
|
let release: (() => Promise<void>) | undefined;
|
|
438
487
|
try {
|
|
439
|
-
const attached = await attachViewer(services,
|
|
488
|
+
const attached = await attachViewer(services, {
|
|
489
|
+
accountId: input.accountId,
|
|
490
|
+
workspaceId: input.workspaceId,
|
|
491
|
+
session: input.session,
|
|
492
|
+
...(input.subjectId ? { viewerSubjectId: input.subjectId } : {}),
|
|
493
|
+
});
|
|
440
494
|
let releasePromise: Promise<void> | undefined;
|
|
441
495
|
release = () =>
|
|
442
496
|
(releasePromise ??= detachViewer(services, {
|
|
@@ -480,7 +534,7 @@ export async function heartbeatViewer(
|
|
|
480
534
|
expectedEpoch: number;
|
|
481
535
|
},
|
|
482
536
|
): Promise<boolean> {
|
|
483
|
-
|
|
537
|
+
const alive = await heartbeatLeaseHolder(services.db, {
|
|
484
538
|
accountId: input.accountId,
|
|
485
539
|
workspaceId: input.workspaceId,
|
|
486
540
|
sandboxGroupId: input.sandboxGroupId,
|
|
@@ -489,6 +543,16 @@ export async function heartbeatViewer(
|
|
|
489
543
|
leaseTtlMs: services.settings.sandboxLeaseTtlMs,
|
|
490
544
|
expectedEpoch: input.expectedEpoch,
|
|
491
545
|
});
|
|
546
|
+
if (!alive) return false;
|
|
547
|
+
const lease = await readLease(services.db, input.workspaceId, input.sandboxGroupId);
|
|
548
|
+
if (lease?.liveness === "warm" && lease.leaseEpoch === input.expectedEpoch && lease.instanceId) {
|
|
549
|
+
await renewSandboxProviderExpiration({
|
|
550
|
+
backend: lease.backend as Settings["sandboxBackend"],
|
|
551
|
+
settings: services.settings,
|
|
552
|
+
instanceId: lease.instanceId,
|
|
553
|
+
}).catch(() => false);
|
|
554
|
+
}
|
|
555
|
+
return true;
|
|
492
556
|
}
|
|
493
557
|
|
|
494
558
|
/**
|
|
@@ -498,7 +562,12 @@ export async function heartbeatViewer(
|
|
|
498
562
|
*/
|
|
499
563
|
export async function detachViewer(
|
|
500
564
|
services: ViewerServices,
|
|
501
|
-
input: {
|
|
565
|
+
input: {
|
|
566
|
+
accountId: string;
|
|
567
|
+
workspaceId: string;
|
|
568
|
+
sandboxGroupId: string;
|
|
569
|
+
viewerId: string;
|
|
570
|
+
},
|
|
502
571
|
): Promise<{ liveness: LeaseSnapshot["liveness"]; refcount: number } | null> {
|
|
503
572
|
return await releaseLeaseHolder(services.db, {
|
|
504
573
|
accountId: input.accountId,
|
|
@@ -625,7 +694,11 @@ export function resolveActiveDesktopTransport(
|
|
|
625
694
|
if (selfhostedActive) {
|
|
626
695
|
return { transport: "relay-frames", client: "frames", mode: "read-only" };
|
|
627
696
|
}
|
|
628
|
-
return {
|
|
697
|
+
return {
|
|
698
|
+
transport: "vnc-ws",
|
|
699
|
+
client: "novnc",
|
|
700
|
+
mode: interactive ? "interactive" : "read-only",
|
|
701
|
+
};
|
|
629
702
|
}
|
|
630
703
|
|
|
631
704
|
/** The minted pixel cell the handshake/attach folds into the DesktopStream
|
|
@@ -639,9 +712,73 @@ export type DesktopStreamMint = {
|
|
|
639
712
|
leaseEpoch: number;
|
|
640
713
|
};
|
|
641
714
|
|
|
715
|
+
/** A subject claim must satisfy the StreamTokenPayload bounds and the 0281
|
|
716
|
+
* holder CHECK (non-blank, <= 512). An out-of-bounds subject (possible only
|
|
717
|
+
* from a host-signed delegated token) is omitted from the claims rather than
|
|
718
|
+
* turning the mint into a 500. */
|
|
719
|
+
function claimableSubjectId(subjectId: string | null): string | null {
|
|
720
|
+
if (!subjectId) return null;
|
|
721
|
+
const trimmed = subjectId.trim();
|
|
722
|
+
return trimmed.length >= 1 && subjectId.length <= 512 ? subjectId : null;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Resolve the viewer-facing authority claims stamped into a scoped stream
|
|
727
|
+
* token and recorded on the viewer holder (0281): the authenticated viewer
|
|
728
|
+
* subject and the session's LIVE authority epoch. Re-verifies a human
|
|
729
|
+
* (`user:`) subject's current workspace authority at mint time through the
|
|
730
|
+
* same model the route-time access builder uses - a membership row with an
|
|
731
|
+
* active owning organization membership, or an active organization
|
|
732
|
+
* membership's personal-workspace pointer - so a viewer whose authority was
|
|
733
|
+
* revoked since the route authorized them degrades to null (transport:null)
|
|
734
|
+
* instead of receiving a fresh token. Delegated token-borne grants
|
|
735
|
+
* (`subjectDelegated`) are authorized by their signed token, not by rows this
|
|
736
|
+
* check can see; they keep their route authorization, as do API-key and
|
|
737
|
+
* service principals. Identity and epoch only - never a secret value.
|
|
738
|
+
*/
|
|
739
|
+
async function resolveViewerAuthorityClaims(
|
|
740
|
+
db: ViewerServices["db"],
|
|
741
|
+
input: {
|
|
742
|
+
accountId: string;
|
|
743
|
+
workspaceId: string;
|
|
744
|
+
sessionId: string;
|
|
745
|
+
subjectId: string | null;
|
|
746
|
+
subjectDelegated?: boolean;
|
|
747
|
+
},
|
|
748
|
+
): Promise<{ subjectId?: string; authorityEpoch: number } | null> {
|
|
749
|
+
const authorityEpoch = await getSessionAuthorityEpoch(db, {
|
|
750
|
+
accountId: input.accountId,
|
|
751
|
+
workspaceId: input.workspaceId,
|
|
752
|
+
sessionId: input.sessionId,
|
|
753
|
+
});
|
|
754
|
+
if (authorityEpoch === null) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
if (input.subjectId?.startsWith("user:") && input.subjectDelegated !== true) {
|
|
758
|
+
const live = await namedSubjectHasLiveWorkspaceAuthority(db, {
|
|
759
|
+
accountId: input.accountId,
|
|
760
|
+
workspaceId: input.workspaceId,
|
|
761
|
+
subjectId: input.subjectId,
|
|
762
|
+
});
|
|
763
|
+
if (!live) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const subjectId = claimableSubjectId(input.subjectId);
|
|
768
|
+
return {
|
|
769
|
+
...(subjectId ? { subjectId } : {}),
|
|
770
|
+
authorityEpoch,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
642
774
|
export type MintDesktopStreamInput = {
|
|
643
775
|
accountId: string;
|
|
644
776
|
workspaceId: string;
|
|
777
|
+
resourceSubjectId?: string;
|
|
778
|
+
/** True when the route grant is a signed delegated token (metadata.delegated):
|
|
779
|
+
* its authority is the token, not membership rows, so the mint-time
|
|
780
|
+
* live-authority recheck does not apply. */
|
|
781
|
+
resourceSubjectDelegated?: boolean;
|
|
645
782
|
session: Session;
|
|
646
783
|
/** The viewer holder id the scoped token is minted for. */
|
|
647
784
|
viewerId: string;
|
|
@@ -704,20 +841,44 @@ export async function mintDesktopStream(
|
|
|
704
841
|
return null;
|
|
705
842
|
}
|
|
706
843
|
|
|
844
|
+
// 0281: the viewer's authority claims. A missing epoch (session not
|
|
845
|
+
// visible) or a removed human membership degrades the mint instead of
|
|
846
|
+
// issuing a fresh token.
|
|
847
|
+
const viewerAuthority = await resolveViewerAuthorityClaims(db, {
|
|
848
|
+
accountId,
|
|
849
|
+
workspaceId,
|
|
850
|
+
sessionId: session.id,
|
|
851
|
+
subjectId: input.resourceSubjectId ?? null,
|
|
852
|
+
...(input.resourceSubjectDelegated !== undefined
|
|
853
|
+
? { subjectDelegated: input.resourceSubjectDelegated }
|
|
854
|
+
: {}),
|
|
855
|
+
});
|
|
856
|
+
if (!viewerAuthority) {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
|
|
707
860
|
// SELFHOSTED ACTIVE: when the session's active sandbox is a selfhosted machine,
|
|
708
861
|
// route to the relay (NOT the Modal group-box path — it would resume the wrong
|
|
709
862
|
// box and return a Modal URL). No Modal lease required.
|
|
710
863
|
if (session.activeSandboxId) {
|
|
711
|
-
const active = await getSandbox(
|
|
864
|
+
const active = await getSandbox(
|
|
865
|
+
db,
|
|
866
|
+
input.resourceSubjectId
|
|
867
|
+
? { accountId, workspaceId, subjectId: input.resourceSubjectId }
|
|
868
|
+
: workspaceId,
|
|
869
|
+
session.activeSandboxId,
|
|
870
|
+
);
|
|
712
871
|
if (active?.kind === "selfhosted") {
|
|
713
872
|
const m = await tryMintActiveSelfhostedStream(
|
|
714
873
|
services,
|
|
715
874
|
{
|
|
716
875
|
session,
|
|
717
876
|
viewerId: input.viewerId,
|
|
718
|
-
workspaceId,
|
|
877
|
+
workspaceId: active.workspaceId,
|
|
878
|
+
...(input.resourceSubjectId ? { resourceSubjectId: input.resourceSubjectId } : {}),
|
|
719
879
|
port: DESKTOP_STREAM_PORT,
|
|
720
880
|
sandbox: active,
|
|
881
|
+
viewerAuthority,
|
|
721
882
|
},
|
|
722
883
|
input.resolveSelfhostedSession,
|
|
723
884
|
);
|
|
@@ -749,7 +910,12 @@ export async function mintDesktopStream(
|
|
|
749
910
|
try {
|
|
750
911
|
// On a cold-restore (the lease's box is gone) this create() must carry the
|
|
751
912
|
// SAME stable run-env the turn declares, so a later turn finds no env delta.
|
|
752
|
-
const environment = await sessionAttachEnvironment(
|
|
913
|
+
const environment = await sessionAttachEnvironment(
|
|
914
|
+
services,
|
|
915
|
+
workspaceId,
|
|
916
|
+
session,
|
|
917
|
+
input.resourceSubjectId ?? null,
|
|
918
|
+
);
|
|
753
919
|
try {
|
|
754
920
|
const establish = () =>
|
|
755
921
|
(services.establishSandboxSession ?? establishSandboxSessionFromEnvelope)(
|
|
@@ -799,6 +965,7 @@ export async function mintDesktopStream(
|
|
|
799
965
|
leaseEpoch: lease.leaseEpoch,
|
|
800
966
|
streamTokenSecret: secret,
|
|
801
967
|
resolution: defaultResolution(settings),
|
|
968
|
+
...viewerAuthority,
|
|
802
969
|
});
|
|
803
970
|
} catch (error) {
|
|
804
971
|
// A transient/headless provider failure degrades the desktop cell.
|
|
@@ -899,6 +1066,9 @@ type SelfhostedStreamMint = Omit<TerminalStreamMint, "transport">;
|
|
|
899
1066
|
export type MintTerminalStreamInput = {
|
|
900
1067
|
accountId: string;
|
|
901
1068
|
workspaceId: string;
|
|
1069
|
+
resourceSubjectId?: string;
|
|
1070
|
+
/** See MintDesktopStreamInput.resourceSubjectDelegated. */
|
|
1071
|
+
resourceSubjectDelegated?: boolean;
|
|
902
1072
|
session: Session;
|
|
903
1073
|
/** The viewer holder / principal id the scoped token is minted for. */
|
|
904
1074
|
viewerId: string;
|
|
@@ -947,20 +1117,44 @@ export async function mintTerminalStream(
|
|
|
947
1117
|
return null;
|
|
948
1118
|
}
|
|
949
1119
|
|
|
1120
|
+
// 0281: the viewer's authority claims. A missing epoch (session not
|
|
1121
|
+
// visible) or a removed human membership degrades the mint instead of
|
|
1122
|
+
// issuing a fresh token.
|
|
1123
|
+
const viewerAuthority = await resolveViewerAuthorityClaims(db, {
|
|
1124
|
+
accountId,
|
|
1125
|
+
workspaceId,
|
|
1126
|
+
sessionId: session.id,
|
|
1127
|
+
subjectId: input.resourceSubjectId ?? null,
|
|
1128
|
+
...(input.resourceSubjectDelegated !== undefined
|
|
1129
|
+
? { subjectDelegated: input.resourceSubjectDelegated }
|
|
1130
|
+
: {}),
|
|
1131
|
+
});
|
|
1132
|
+
if (!viewerAuthority) {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
950
1136
|
// SELFHOSTED ACTIVE: when the session's active sandbox is a selfhosted machine,
|
|
951
1137
|
// route to the relay. NEVER fall through to the Modal group-box path (it would
|
|
952
1138
|
// resume the wrong box / return a Modal URL).
|
|
953
1139
|
if (session.activeSandboxId) {
|
|
954
|
-
const active = await getSandbox(
|
|
1140
|
+
const active = await getSandbox(
|
|
1141
|
+
db,
|
|
1142
|
+
input.resourceSubjectId
|
|
1143
|
+
? { accountId, workspaceId, subjectId: input.resourceSubjectId }
|
|
1144
|
+
: workspaceId,
|
|
1145
|
+
session.activeSandboxId,
|
|
1146
|
+
);
|
|
955
1147
|
if (active?.kind === "selfhosted") {
|
|
956
1148
|
const stream = await tryMintActiveSelfhostedStream(
|
|
957
1149
|
services,
|
|
958
1150
|
{
|
|
959
1151
|
session,
|
|
960
1152
|
viewerId: input.viewerId,
|
|
961
|
-
workspaceId,
|
|
1153
|
+
workspaceId: active.workspaceId,
|
|
1154
|
+
...(input.resourceSubjectId ? { resourceSubjectId: input.resourceSubjectId } : {}),
|
|
962
1155
|
port: TERMINAL_STREAM_PORT,
|
|
963
1156
|
sandbox: active,
|
|
1157
|
+
viewerAuthority,
|
|
964
1158
|
},
|
|
965
1159
|
input.resolveSelfhostedSession,
|
|
966
1160
|
);
|
|
@@ -982,7 +1176,12 @@ export async function mintTerminalStream(
|
|
|
982
1176
|
try {
|
|
983
1177
|
// On a cold-restore this create() must carry the SAME stable run-env the turn
|
|
984
1178
|
// declares, so a later turn finds no manifest-env delta.
|
|
985
|
-
const environment = await sessionAttachEnvironment(
|
|
1179
|
+
const environment = await sessionAttachEnvironment(
|
|
1180
|
+
services,
|
|
1181
|
+
workspaceId,
|
|
1182
|
+
session,
|
|
1183
|
+
input.resourceSubjectId ?? null,
|
|
1184
|
+
);
|
|
986
1185
|
try {
|
|
987
1186
|
const establish = () =>
|
|
988
1187
|
(services.establishSandboxSession ?? establishSandboxSessionFromEnvelope)(
|
|
@@ -1008,7 +1207,9 @@ export async function mintTerminalStream(
|
|
|
1008
1207
|
// Idempotent ttyd launch (flock-guarded; a no-op when already up). A box that
|
|
1009
1208
|
// genuinely can't run it degrades to the sse-events firehose, not a throw.
|
|
1010
1209
|
try {
|
|
1011
|
-
await ensureTerminalServer(established.session, {
|
|
1210
|
+
await ensureTerminalServer(established.session, {
|
|
1211
|
+
port: TERMINAL_STREAM_PORT,
|
|
1212
|
+
});
|
|
1012
1213
|
} catch (error) {
|
|
1013
1214
|
if (error instanceof TerminalServerUnsupportedError) {
|
|
1014
1215
|
return null;
|
|
@@ -1029,6 +1230,7 @@ export async function mintTerminalStream(
|
|
|
1029
1230
|
leaseEpoch: lease.leaseEpoch,
|
|
1030
1231
|
streamTokenSecret: secret,
|
|
1031
1232
|
port: TERMINAL_STREAM_PORT,
|
|
1233
|
+
...viewerAuthority,
|
|
1032
1234
|
});
|
|
1033
1235
|
} catch (error) {
|
|
1034
1236
|
if (error instanceof StreamPortUnavailableError) {
|
|
@@ -1104,8 +1306,11 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1104
1306
|
session: Session;
|
|
1105
1307
|
viewerId: string;
|
|
1106
1308
|
workspaceId: string;
|
|
1309
|
+
resourceSubjectId?: string;
|
|
1107
1310
|
port: number;
|
|
1108
1311
|
sandbox: SandboxRecord;
|
|
1312
|
+
/** 0281 viewer authority claims stamped into the relay stream token. */
|
|
1313
|
+
viewerAuthority?: { subjectId?: string; authorityEpoch: number };
|
|
1109
1314
|
},
|
|
1110
1315
|
// optional test seam (mirrors the existing `establish?` seam pattern): inject a
|
|
1111
1316
|
// fake relay-resolving session; production NEVER passes it.
|
|
@@ -1127,11 +1332,26 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1127
1332
|
if (resolveSelfhostedSession) {
|
|
1128
1333
|
shSession = await resolveSelfhostedSession(sandbox);
|
|
1129
1334
|
} else {
|
|
1335
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
1336
|
+
services.db,
|
|
1337
|
+
input.resourceSubjectId
|
|
1338
|
+
? {
|
|
1339
|
+
accountId: input.session.accountId,
|
|
1340
|
+
workspaceId: input.session.workspaceId,
|
|
1341
|
+
subjectId: input.resourceSubjectId,
|
|
1342
|
+
}
|
|
1343
|
+
: workspaceId,
|
|
1344
|
+
sandbox.enrollmentId,
|
|
1345
|
+
);
|
|
1346
|
+
if (!enrollment?.connectionInstanceId) return null;
|
|
1347
|
+
if (!enrollment.workspaceRoot) return null;
|
|
1130
1348
|
const client = new SelfhostedSandboxClient({
|
|
1131
1349
|
workspaceId,
|
|
1350
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
1132
1351
|
relay: relayConfigFromSettings(settings),
|
|
1133
1352
|
controlRpcFactory: () => controlRpc(bus),
|
|
1134
1353
|
agentId: sandbox.enrollmentId,
|
|
1354
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
1135
1355
|
epoch: session.activeEpoch,
|
|
1136
1356
|
// Stream authority rotates; terminal process identity does not.
|
|
1137
1357
|
terminalScopeId: session.id,
|
|
@@ -1151,6 +1371,7 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1151
1371
|
sessionId: session.id,
|
|
1152
1372
|
viewerId: input.viewerId,
|
|
1153
1373
|
activeEpoch: session.activeEpoch,
|
|
1374
|
+
...(input.viewerAuthority ? { viewerAuthority: input.viewerAuthority } : {}),
|
|
1154
1375
|
port,
|
|
1155
1376
|
session: shSession,
|
|
1156
1377
|
});
|
|
@@ -1170,6 +1391,8 @@ export type MintSelfhostedStreamInput = {
|
|
|
1170
1391
|
* THIS as its leaseEpoch claim so the relay rejects a stale-epoch (swapped-away)
|
|
1171
1392
|
* viewer. */
|
|
1172
1393
|
activeEpoch: number;
|
|
1394
|
+
/** 0281 viewer authority claims stamped into the relay stream token. */
|
|
1395
|
+
viewerAuthority?: { subjectId?: string; authorityEpoch: number };
|
|
1173
1396
|
/** The exposed stream port (6080 desktop / 7681 terminal). */
|
|
1174
1397
|
port: number;
|
|
1175
1398
|
/** The resolvable selfhosted session (the routing proxy resolves the active
|
|
@@ -1213,6 +1436,7 @@ export async function mintSelfhostedStream(
|
|
|
1213
1436
|
leaseEpoch: input.activeEpoch,
|
|
1214
1437
|
streamTokenSecret: secret,
|
|
1215
1438
|
port: input.port,
|
|
1439
|
+
...(input.viewerAuthority ?? {}),
|
|
1216
1440
|
});
|
|
1217
1441
|
return {
|
|
1218
1442
|
url: exposed.url,
|