@opengeni/api-router 0.5.3 → 0.5.4
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.js +1 -1
- package/dist/{chunk-3HIA43CC.js → chunk-DO2G3JSB.js} +5184 -2195
- package/dist/chunk-DO2G3JSB.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +297 -54
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
- package/src/app.ts +415 -147
- package/src/auth/managed-auth.ts +32 -16
- package/src/http/auth.ts +8 -1
- package/src/http/common.ts +6 -2
- package/src/http/sse.ts +27 -6
- package/src/index.ts +196 -74
- package/src/integrations/oauth-client.ts +403 -120
- package/src/integrations/provider-domain.ts +4 -1
- package/src/mcp/documents.ts +173 -94
- package/src/mcp/server.ts +1517 -692
- package/src/mcp/session-view.ts +8 -2
- package/src/mcp/toolspace.ts +175 -84
- package/src/observability.ts +7 -1
- package/src/routes/api-keys.ts +39 -23
- package/src/routes/billing.ts +180 -65
- package/src/routes/capabilities.ts +17 -8
- package/src/routes/catalog-assets.ts +5 -2
- package/src/routes/codex.ts +244 -63
- package/src/routes/connections.ts +71 -33
- package/src/routes/documents.ts +242 -92
- package/src/routes/enrollments.ts +100 -70
- package/src/routes/environments.ts +205 -136
- package/src/routes/files.ts +164 -39
- package/src/routes/github.ts +123 -50
- package/src/routes/install.ts +9 -2
- package/src/routes/machines.ts +9 -8
- package/src/routes/packs.ts +141 -89
- package/src/routes/rigs.ts +189 -0
- package/src/routes/scheduled-tasks.ts +51 -9
- package/src/routes/sessions.ts +839 -329
- package/src/routes/social.ts +50 -38
- package/src/routes/workspace-capture.ts +238 -0
- package/src/routes/workspaces.ts +159 -13
- package/src/sandbox/access.ts +11 -3
- package/src/sandbox/auth-callout.ts +5 -1
- package/src/sandbox/channel-a.ts +104 -27
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +68 -59
- package/src/sandbox/metrics-ingestion.ts +238 -17
- package/src/sandbox/viewer.ts +172 -46
- package/dist/chunk-3HIA43CC.js.map +0 -1
package/src/sandbox/viewer.ts
CHANGED
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
// lease's recorded data_plane_url (null until P4 mints it).
|
|
20
20
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
applyGitAuthPointerEnvironment,
|
|
24
|
+
hasGitCredentialRepositorySelection,
|
|
25
|
+
hasGitHubRepositorySelection,
|
|
26
|
+
resolveStreamTokenSecret,
|
|
27
|
+
stableSandboxEnvironmentForRun,
|
|
28
|
+
} from "@opengeni/config";
|
|
23
29
|
import type { Settings } from "@opengeni/config";
|
|
24
30
|
import { githubAppBotIdentity } from "@opengeni/github";
|
|
25
31
|
import { type Session, type StreamUrlRotatedPayload } from "@opengeni/contracts";
|
|
@@ -31,6 +37,7 @@ import {
|
|
|
31
37
|
getSandboxSessionEnvelope,
|
|
32
38
|
heartbeatLeaseHolder,
|
|
33
39
|
loadWorkspaceEnvironmentForRun,
|
|
40
|
+
markWarmLeaseInstanceLost,
|
|
34
41
|
readLease,
|
|
35
42
|
recordLeaseDataPlaneUrl,
|
|
36
43
|
recordLeaseTerminalDataPlaneUrl,
|
|
@@ -50,6 +57,8 @@ import {
|
|
|
50
57
|
ensureDisplayStack,
|
|
51
58
|
ensureTerminalServer,
|
|
52
59
|
establishSandboxSessionFromEnvelope,
|
|
60
|
+
isProviderSandboxNotFoundError,
|
|
61
|
+
SandboxResumeStateUnavailableError,
|
|
53
62
|
exposeStreamPort,
|
|
54
63
|
desktopCapableBackend,
|
|
55
64
|
NatsControlRpc,
|
|
@@ -133,12 +142,22 @@ export async function sessionAttachEnvironment(
|
|
|
133
142
|
// attach env keyed off the deployment default would cold-create e.g. an e2b
|
|
134
143
|
// session's box with /workspace-rooted values while its turn declares
|
|
135
144
|
// /home/user-rooted ones — the same guard-killed first turn all over again.
|
|
136
|
-
const settingsForSession =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
const settingsForSession =
|
|
146
|
+
session.sandboxBackend !== services.settings.sandboxBackend
|
|
147
|
+
? { ...services.settings, sandboxBackend: session.sandboxBackend }
|
|
148
|
+
: services.settings;
|
|
149
|
+
const environment = stableSandboxEnvironmentForRun(
|
|
150
|
+
settingsForSession,
|
|
151
|
+
workspaceEnvironment?.values ?? {},
|
|
152
|
+
{ workspaceId },
|
|
153
|
+
);
|
|
154
|
+
if (hasGitCredentialRepositorySelection(session.resources)) {
|
|
155
|
+
applyGitAuthPointerEnvironment(
|
|
156
|
+
environment,
|
|
157
|
+
hasGitHubRepositorySelection(session.resources)
|
|
158
|
+
? githubAppBotIdentity(services.settings)
|
|
159
|
+
: null,
|
|
160
|
+
);
|
|
142
161
|
}
|
|
143
162
|
return environment;
|
|
144
163
|
}
|
|
@@ -182,13 +201,16 @@ export async function attachViewer(
|
|
|
182
201
|
backend: session.sandboxBackend,
|
|
183
202
|
os: session.sandboxOs,
|
|
184
203
|
leaseTtlMs,
|
|
204
|
+
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
185
205
|
});
|
|
186
206
|
|
|
187
207
|
// FENCED: a newer epoch re-established the box. Release our just-registered
|
|
188
208
|
// holder and surface a 409 — the client re-reads capabilities and re-attaches.
|
|
189
209
|
if (acquired.role === "fenced") {
|
|
190
210
|
await release();
|
|
191
|
-
throw new HTTPException(409, {
|
|
211
|
+
throw new HTTPException(409, {
|
|
212
|
+
message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach`,
|
|
213
|
+
});
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
// SPAWNER: we won the cold->warming CAS. Establish the box in-process from the
|
|
@@ -216,6 +238,7 @@ export async function attachViewer(
|
|
|
216
238
|
const spawnEnvelope = acquired.lease.resumeState ?? envelope;
|
|
217
239
|
established = await establishSandboxSessionFromEnvelope(settings, spawnEnvelope, {
|
|
218
240
|
sessionId: session.id,
|
|
241
|
+
recovery: "create-or-restore",
|
|
219
242
|
backendOverride: session.sandboxBackend,
|
|
220
243
|
environment,
|
|
221
244
|
});
|
|
@@ -224,7 +247,8 @@ export async function attachViewer(
|
|
|
224
247
|
// call, the reaper) resumes THIS box by id instead of cold-creating a
|
|
225
248
|
// rival. Fall back to the session envelope only when serialize is
|
|
226
249
|
// unavailable. (Without this the box churned: each op spawned its own box.)
|
|
227
|
-
const resumeEnvelope =
|
|
250
|
+
const resumeEnvelope =
|
|
251
|
+
(await serializeEstablishedSandboxEnvelope(established)) ?? envelope ?? null;
|
|
228
252
|
const committed = await commitWarmingToWarm(db, {
|
|
229
253
|
accountId,
|
|
230
254
|
workspaceId,
|
|
@@ -254,7 +278,9 @@ export async function attachViewer(
|
|
|
254
278
|
};
|
|
255
279
|
} catch (error) {
|
|
256
280
|
if (error instanceof SandboxLeaseSupersededError) {
|
|
257
|
-
throw new HTTPException(409, {
|
|
281
|
+
throw new HTTPException(409, {
|
|
282
|
+
message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach`,
|
|
283
|
+
});
|
|
258
284
|
}
|
|
259
285
|
// Caught spawn failure: roll the warming row back to cold so the next
|
|
260
286
|
// arrival (a turn or another viewer) re-acquires and re-spawns. Holders
|
|
@@ -268,7 +294,9 @@ export async function attachViewer(
|
|
|
268
294
|
// to cold, so a re-attach re-acquires and re-spawns. Preserve an already-typed
|
|
269
295
|
// HTTPException unchanged.
|
|
270
296
|
if (error instanceof HTTPException) throw error;
|
|
271
|
-
throw new HTTPException(409, {
|
|
297
|
+
throw new HTTPException(409, {
|
|
298
|
+
message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})`,
|
|
299
|
+
});
|
|
272
300
|
} finally {
|
|
273
301
|
// Drop the in-process handle: the API resumed BY ID for the cold-spawn,
|
|
274
302
|
// it does NOT own the box. The lease's refcount (this viewer holder) keeps
|
|
@@ -297,7 +325,13 @@ export async function attachViewer(
|
|
|
297
325
|
*/
|
|
298
326
|
export async function heartbeatViewer(
|
|
299
327
|
services: ViewerServices,
|
|
300
|
-
input: {
|
|
328
|
+
input: {
|
|
329
|
+
accountId: string;
|
|
330
|
+
workspaceId: string;
|
|
331
|
+
sandboxGroupId: string;
|
|
332
|
+
viewerId: string;
|
|
333
|
+
expectedEpoch: number;
|
|
334
|
+
},
|
|
301
335
|
): Promise<boolean> {
|
|
302
336
|
return await heartbeatLeaseHolder(services.db, {
|
|
303
337
|
accountId: input.accountId,
|
|
@@ -357,12 +391,52 @@ export function viewerHeartbeatIntervalMs(settings: Settings): number {
|
|
|
357
391
|
// (the lease showed warm while Modal showed the box gone; reads 404'd against a
|
|
358
392
|
// fresh box). We therefore DO NOT call session.close()/shutdown()/delete() — the
|
|
359
393
|
// reaper (provider stop at refcount 0) is the ONLY sanctioned box terminator.
|
|
360
|
-
async function dropEstablishedHandle(
|
|
394
|
+
async function dropEstablishedHandle(
|
|
395
|
+
established: EstablishedSandboxSession | undefined,
|
|
396
|
+
): Promise<void> {
|
|
361
397
|
// Intentionally a no-op beyond dropping the reference: terminating the box here
|
|
362
398
|
// is wrong (see above). The lease owns lifecycle; the reaper owns teardown.
|
|
363
399
|
void established;
|
|
364
400
|
}
|
|
365
401
|
|
|
402
|
+
/** A stream resolver is an attached observer, never a box creator. If its
|
|
403
|
+
* resume-only call proves the exact warm provider instance gone, retire that
|
|
404
|
+
* epoch atomically so the next ordinary attach elects one replacement owner. */
|
|
405
|
+
async function retireMissingWarmLease(
|
|
406
|
+
services: ViewerServices,
|
|
407
|
+
input: {
|
|
408
|
+
accountId: string;
|
|
409
|
+
workspaceId: string;
|
|
410
|
+
session: Session;
|
|
411
|
+
lease: LeaseSnapshot;
|
|
412
|
+
},
|
|
413
|
+
error: unknown,
|
|
414
|
+
): Promise<boolean> {
|
|
415
|
+
if (
|
|
416
|
+
input.lease.instanceId === null ||
|
|
417
|
+
(!(error instanceof SandboxResumeStateUnavailableError) &&
|
|
418
|
+
!isProviderSandboxNotFoundError(input.session.sandboxBackend, error))
|
|
419
|
+
) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const marked = await markWarmLeaseInstanceLost(services.db, {
|
|
423
|
+
accountId: input.accountId,
|
|
424
|
+
workspaceId: input.workspaceId,
|
|
425
|
+
sandboxGroupId: input.session.sandboxGroupId,
|
|
426
|
+
expectedEpoch: input.lease.leaseEpoch,
|
|
427
|
+
expectedInstanceId: input.lease.instanceId,
|
|
428
|
+
});
|
|
429
|
+
if (marked.status === "marked" && services.bus) {
|
|
430
|
+
await appendAndPublishEvents(services.db, services.bus, input.workspaceId, input.session.id, [
|
|
431
|
+
{
|
|
432
|
+
type: "sandbox.box.lost",
|
|
433
|
+
payload: { sandboxId: input.lease.instanceId },
|
|
434
|
+
},
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
366
440
|
// ============================================================================
|
|
367
441
|
// P4.2 — the pixel DATA PLANE, served API-DIRECT.
|
|
368
442
|
//
|
|
@@ -397,7 +471,11 @@ async function dropEstablishedHandle(established: EstablishedSandboxSession | un
|
|
|
397
471
|
export function resolveActiveDesktopTransport(
|
|
398
472
|
selfhostedActive: boolean,
|
|
399
473
|
interactive: boolean,
|
|
400
|
-
): {
|
|
474
|
+
): {
|
|
475
|
+
transport: "relay-frames" | "vnc-ws";
|
|
476
|
+
client: "frames" | "novnc";
|
|
477
|
+
mode: "read-only" | "interactive";
|
|
478
|
+
} {
|
|
401
479
|
if (selfhostedActive) {
|
|
402
480
|
return { transport: "relay-frames", client: "frames", mode: "read-only" };
|
|
403
481
|
}
|
|
@@ -432,12 +510,12 @@ export type MintDesktopStreamInput = {
|
|
|
432
510
|
* real leaf `establishSandboxSessionFromEnvelope`. Production NEVER passes
|
|
433
511
|
* this; it exists so a real-lease integration test can inject a fake provider
|
|
434
512
|
* session carrying `resolveExposedPort` without a live cloud box. */
|
|
435
|
-
establish?: (
|
|
436
|
-
envelope: Record<string, unknown> | null,
|
|
437
|
-
) => Promise<EstablishedSandboxSession>;
|
|
513
|
+
establish?: (envelope: Record<string, unknown> | null) => Promise<EstablishedSandboxSession>;
|
|
438
514
|
/** Test seam: inject a fake relay-resolving session for the selfhosted-active
|
|
439
515
|
* branch. Production NEVER passes this. */
|
|
440
|
-
resolveSelfhostedSession?: (
|
|
516
|
+
resolveSelfhostedSession?: (
|
|
517
|
+
sandbox: SandboxRecord,
|
|
518
|
+
) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>;
|
|
441
519
|
};
|
|
442
520
|
|
|
443
521
|
/**
|
|
@@ -486,9 +564,27 @@ export async function mintDesktopStream(
|
|
|
486
564
|
if (session.activeSandboxId) {
|
|
487
565
|
const active = await getSandbox(db, workspaceId, session.activeSandboxId);
|
|
488
566
|
if (active?.kind === "selfhosted") {
|
|
489
|
-
const m = await tryMintActiveSelfhostedStream(
|
|
567
|
+
const m = await tryMintActiveSelfhostedStream(
|
|
568
|
+
services,
|
|
569
|
+
{
|
|
570
|
+
session,
|
|
571
|
+
viewerId: input.viewerId,
|
|
572
|
+
workspaceId,
|
|
573
|
+
port: DESKTOP_STREAM_PORT,
|
|
574
|
+
sandbox: active,
|
|
575
|
+
},
|
|
576
|
+
input.resolveSelfhostedSession,
|
|
577
|
+
);
|
|
490
578
|
// mintSelfhostedStream returns no resolution; the desktop cell needs it.
|
|
491
|
-
return m
|
|
579
|
+
return m
|
|
580
|
+
? {
|
|
581
|
+
url: m.url,
|
|
582
|
+
token: m.token,
|
|
583
|
+
expiresAt: m.expiresAt,
|
|
584
|
+
resolution: defaultResolution(settings),
|
|
585
|
+
leaseEpoch: m.leaseEpoch,
|
|
586
|
+
}
|
|
587
|
+
: null;
|
|
492
588
|
}
|
|
493
589
|
// A Modal swap target (or unknown) falls through to the existing group-box path.
|
|
494
590
|
}
|
|
@@ -526,22 +622,28 @@ export async function mintDesktopStream(
|
|
|
526
622
|
};
|
|
527
623
|
}
|
|
528
624
|
|
|
529
|
-
// Resume the LIVE box by id
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
const envelope = lease.resumeState
|
|
625
|
+
// Resume the LIVE box by id from the authoritative lease descriptor. An
|
|
626
|
+
// attached stream resolver has no creation authority and never substitutes a
|
|
627
|
+
// per-session envelope for missing lease state.
|
|
628
|
+
const envelope = lease.resumeState;
|
|
533
629
|
let established: EstablishedSandboxSession | undefined;
|
|
534
630
|
try {
|
|
535
631
|
// On a cold-restore (the lease's box is gone) this create() must carry the
|
|
536
632
|
// SAME stable run-env the turn declares, so a later turn finds no env delta.
|
|
537
633
|
const environment = await sessionAttachEnvironment(services, workspaceId, session);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
634
|
+
try {
|
|
635
|
+
established = input.establish
|
|
636
|
+
? await input.establish(envelope)
|
|
637
|
+
: await establishSandboxSessionFromEnvelope(settings, envelope, {
|
|
638
|
+
sessionId: session.id,
|
|
639
|
+
recovery: "resume-only",
|
|
640
|
+
backendOverride: session.sandboxBackend,
|
|
641
|
+
environment,
|
|
642
|
+
});
|
|
643
|
+
} catch (error) {
|
|
644
|
+
await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
545
647
|
|
|
546
648
|
// Idempotent display stack (flock-guarded; a no-op when already up). A box
|
|
547
649
|
// that genuinely can't run the stack degrades to transport:null, not a throw.
|
|
@@ -666,12 +768,12 @@ export type MintTerminalStreamInput = {
|
|
|
666
768
|
lease?: LeaseSnapshot;
|
|
667
769
|
/** Test seam: override how the box is re-established by id (see
|
|
668
770
|
* MintDesktopStreamInput.establish). Production NEVER passes this. */
|
|
669
|
-
establish?: (
|
|
670
|
-
envelope: Record<string, unknown> | null,
|
|
671
|
-
) => Promise<EstablishedSandboxSession>;
|
|
771
|
+
establish?: (envelope: Record<string, unknown> | null) => Promise<EstablishedSandboxSession>;
|
|
672
772
|
/** Test seam: inject a fake relay-resolving session for the selfhosted-active
|
|
673
773
|
* branch. Production NEVER passes this. */
|
|
674
|
-
resolveSelfhostedSession?: (
|
|
774
|
+
resolveSelfhostedSession?: (
|
|
775
|
+
sandbox: SandboxRecord,
|
|
776
|
+
) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>;
|
|
675
777
|
};
|
|
676
778
|
|
|
677
779
|
/**
|
|
@@ -712,7 +814,17 @@ export async function mintTerminalStream(
|
|
|
712
814
|
if (session.activeSandboxId) {
|
|
713
815
|
const active = await getSandbox(db, workspaceId, session.activeSandboxId);
|
|
714
816
|
if (active?.kind === "selfhosted") {
|
|
715
|
-
return await tryMintActiveSelfhostedStream(
|
|
817
|
+
return await tryMintActiveSelfhostedStream(
|
|
818
|
+
services,
|
|
819
|
+
{
|
|
820
|
+
session,
|
|
821
|
+
viewerId: input.viewerId,
|
|
822
|
+
workspaceId,
|
|
823
|
+
port: TERMINAL_STREAM_PORT,
|
|
824
|
+
sandbox: active,
|
|
825
|
+
},
|
|
826
|
+
input.resolveSelfhostedSession,
|
|
827
|
+
);
|
|
716
828
|
}
|
|
717
829
|
// A Modal swap target (or unknown) falls through to the existing group-box path
|
|
718
830
|
// (unchanged — Modal swap-target streaming is out of scope for this fix).
|
|
@@ -748,19 +860,25 @@ export async function mintTerminalStream(
|
|
|
748
860
|
|
|
749
861
|
// Resume the LIVE box by id (lease.resume_state authoritative), ensure ttyd, and
|
|
750
862
|
// resolve the 7681 tunnel + mint the scoped token, IN-PROCESS.
|
|
751
|
-
const envelope = lease.resumeState
|
|
863
|
+
const envelope = lease.resumeState;
|
|
752
864
|
let established: EstablishedSandboxSession | undefined;
|
|
753
865
|
try {
|
|
754
866
|
// On a cold-restore this create() must carry the SAME stable run-env the turn
|
|
755
867
|
// declares, so a later turn finds no manifest-env delta.
|
|
756
868
|
const environment = await sessionAttachEnvironment(services, workspaceId, session);
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
869
|
+
try {
|
|
870
|
+
established = input.establish
|
|
871
|
+
? await input.establish(envelope)
|
|
872
|
+
: await establishSandboxSessionFromEnvelope(settings, envelope, {
|
|
873
|
+
sessionId: session.id,
|
|
874
|
+
recovery: "resume-only",
|
|
875
|
+
backendOverride: session.sandboxBackend,
|
|
876
|
+
environment,
|
|
877
|
+
});
|
|
878
|
+
} catch (error) {
|
|
879
|
+
await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
764
882
|
|
|
765
883
|
// Idempotent ttyd launch (flock-guarded; a no-op when already up). A box that
|
|
766
884
|
// genuinely can't run it degrades to the sse-events firehose, not a throw.
|
|
@@ -853,10 +971,18 @@ function controlRpc(bus: EventBus | undefined): ControlRpc {
|
|
|
853
971
|
*/
|
|
854
972
|
async function tryMintActiveSelfhostedStream(
|
|
855
973
|
services: ViewerServices,
|
|
856
|
-
input: {
|
|
974
|
+
input: {
|
|
975
|
+
session: Session;
|
|
976
|
+
viewerId: string;
|
|
977
|
+
workspaceId: string;
|
|
978
|
+
port: number;
|
|
979
|
+
sandbox: SandboxRecord;
|
|
980
|
+
},
|
|
857
981
|
// optional test seam (mirrors the existing `establish?` seam pattern): inject a
|
|
858
982
|
// fake relay-resolving session; production NEVER passes it.
|
|
859
|
-
resolveSelfhostedSession?: (
|
|
983
|
+
resolveSelfhostedSession?: (
|
|
984
|
+
sandbox: SandboxRecord,
|
|
985
|
+
) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>,
|
|
860
986
|
): Promise<TerminalStreamMint | null> {
|
|
861
987
|
const { settings, bus } = services;
|
|
862
988
|
const { session, workspaceId, port, sandbox } = input;
|