@opengeni/api-router 0.22.2 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
|
@@ -75,8 +75,9 @@ export const DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
|
75
75
|
// box) caused a self-hosted agent to drop PERMANENTLY one hour after connecting: the
|
|
76
76
|
// bearer expired and the auth-callout rejected every reconnect ("re-enroll may be
|
|
77
77
|
// required"). A long-lived bearer is safe because the auth-callout RE-CHECKS the
|
|
78
|
-
// enrollment status on every (re)connect
|
|
79
|
-
//
|
|
78
|
+
// enrollment status AND credential generation on every (re)connect
|
|
79
|
+
// (auth-callout.ts) — a revoked machine or an old pre-re-enrollment bearer is denied
|
|
80
|
+
// regardless of bearer life. The short NATS user-JWT cap bounds already-live access.
|
|
80
81
|
export const ENROLLMENT_BEARER_TTL_SECONDS = 30 * 24 * 3600;
|
|
81
82
|
// The relay PRODUCER token (the `ogr_` token; M8b) is ENROLLMENT-scoped,
|
|
82
83
|
// NOT per-stream: the agent presents it on every channel registration for the life
|
|
@@ -370,6 +371,7 @@ export async function exchangeEnrollToken(
|
|
|
370
371
|
secret,
|
|
371
372
|
workspaceId: claims.workspaceId,
|
|
372
373
|
agentId: enrollment.id,
|
|
374
|
+
credentialGeneration: enrollment.credentialGeneration,
|
|
373
375
|
consentedScreenControl: enrollment.allowScreenControl,
|
|
374
376
|
});
|
|
375
377
|
return { ok: true, credentials };
|
|
@@ -430,6 +432,7 @@ export async function pollDeviceEnrollment(
|
|
|
430
432
|
secret,
|
|
431
433
|
workspaceId: request.workspaceId,
|
|
432
434
|
agentId: enrollment.id,
|
|
435
|
+
credentialGeneration: enrollment.credentialGeneration,
|
|
433
436
|
consentedScreenControl: enrollment.allowScreenControl,
|
|
434
437
|
});
|
|
435
438
|
|
|
@@ -455,7 +458,13 @@ export async function pollDeviceEnrollment(
|
|
|
455
458
|
* bearer so an agent using it as the connect-token credential works uniformly. */
|
|
456
459
|
async function buildEnrollmentCredentials(
|
|
457
460
|
services: EnrollmentServices,
|
|
458
|
-
input: {
|
|
461
|
+
input: {
|
|
462
|
+
secret: string;
|
|
463
|
+
workspaceId: string;
|
|
464
|
+
agentId: string;
|
|
465
|
+
credentialGeneration: number;
|
|
466
|
+
consentedScreenControl: boolean;
|
|
467
|
+
},
|
|
459
468
|
): Promise<EnrollmentCredentialsResponse> {
|
|
460
469
|
const { settings } = services;
|
|
461
470
|
// The control-plane subject prefix the agent subscribes to: agent.<ws>.<id>.
|
|
@@ -466,6 +475,7 @@ async function buildEnrollmentCredentials(
|
|
|
466
475
|
workspaceId: input.workspaceId,
|
|
467
476
|
agentId: input.agentId,
|
|
468
477
|
enrollmentId: input.agentId,
|
|
478
|
+
credentialGeneration: input.credentialGeneration,
|
|
469
479
|
subjectPrefix,
|
|
470
480
|
exp,
|
|
471
481
|
});
|
package/src/sandbox/machines.ts
CHANGED
|
@@ -219,7 +219,7 @@ export async function listMachines(
|
|
|
219
219
|
// onto the machines (no N+1). Each machine is probed for liveness.
|
|
220
220
|
const [sandboxes, enrollments, metricsByEnrollment] = await Promise.all([
|
|
221
221
|
listSandboxes(db, workspaceId),
|
|
222
|
-
listEnrollments(db, workspaceId),
|
|
222
|
+
listEnrollments(db, workspaceId, { status: "active" }),
|
|
223
223
|
readMachineMetricsLatestForWorkspace(db, workspaceId),
|
|
224
224
|
]);
|
|
225
225
|
const enrollmentById = new Map(enrollments.map((e) => [e.id, e]));
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
import {
|
|
34
34
|
clearEnrollmentWentOffline,
|
|
35
|
+
disconnectAttachedBrowserDevices,
|
|
35
36
|
getEnrollment,
|
|
36
37
|
ingestMachineMetricsSample,
|
|
38
|
+
reconcileAttachedBrowserInventory,
|
|
37
39
|
sessionsWithActiveOpOnEnrollment,
|
|
38
40
|
setEnrollmentDisplayState,
|
|
39
41
|
setEnrollmentOpStreamState,
|
|
@@ -43,13 +45,20 @@ import {
|
|
|
43
45
|
type Database,
|
|
44
46
|
type MachineMetricsSample,
|
|
45
47
|
} from "@opengeni/db";
|
|
48
|
+
import {
|
|
49
|
+
AttachedBrowserInventorySnapshot as AttachedBrowserInventoryContract,
|
|
50
|
+
type AttachedBrowserInventorySnapshot as AttachedBrowserInventoryContractValue,
|
|
51
|
+
} from "@opengeni/contracts";
|
|
46
52
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
47
53
|
import type { Observability } from "@opengeni/observability";
|
|
48
54
|
import {
|
|
49
55
|
AgentEvent,
|
|
56
|
+
Arch,
|
|
50
57
|
GoingOfflineReason,
|
|
51
58
|
Hello,
|
|
59
|
+
Os,
|
|
52
60
|
goingOfflineReasonToJSON,
|
|
61
|
+
type AttachedBrowserInventorySnapshot as WireAttachedBrowserInventorySnapshot,
|
|
53
62
|
type MetricsSample,
|
|
54
63
|
} from "@opengeni/agent-proto";
|
|
55
64
|
|
|
@@ -124,6 +133,90 @@ export function wireSampleToDbSample(wire: MetricsSample): MachineMetricsSample
|
|
|
124
133
|
};
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
/** Validate and project one wire inventory before it reaches durable browser
|
|
137
|
+
* discovery. Unknown enum values and unsafe uint64 counters reject the whole
|
|
138
|
+
* authoritative snapshot instead of partially disconnecting good endpoints. */
|
|
139
|
+
export function wireAttachedBrowserInventoryToContract(
|
|
140
|
+
wire: WireAttachedBrowserInventorySnapshot,
|
|
141
|
+
): AttachedBrowserInventoryContractValue {
|
|
142
|
+
return AttachedBrowserInventoryContract.parse({
|
|
143
|
+
bridgeGeneration: wire.bridgeGeneration,
|
|
144
|
+
revision: safeWireInteger(wire.revision, "browser inventory revision"),
|
|
145
|
+
devices: wire.devices.map((device) => ({
|
|
146
|
+
id: device.id,
|
|
147
|
+
name: device.name,
|
|
148
|
+
profileLabel: device.profileLabel.trim() || null,
|
|
149
|
+
browserName: device.browserName,
|
|
150
|
+
browserVersion: device.browserVersion,
|
|
151
|
+
extensionVersion: device.extensionVersion,
|
|
152
|
+
platform: wireBrowserPlatform(device.platform),
|
|
153
|
+
architecture: wireBrowserArchitecture(device.arch),
|
|
154
|
+
connectionGeneration: device.connectionGeneration,
|
|
155
|
+
inventoryRevision: safeWireInteger(
|
|
156
|
+
device.inventoryRevision,
|
|
157
|
+
"attached browser inventory revision",
|
|
158
|
+
),
|
|
159
|
+
tabCount: safeWireInteger(device.tabCount, "attached browser tab count"),
|
|
160
|
+
capabilities: device.capabilities,
|
|
161
|
+
})),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function safeWireInteger(value: string | number, label: string): number {
|
|
166
|
+
let parsed: bigint;
|
|
167
|
+
try {
|
|
168
|
+
parsed = BigInt(value);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error(`${label} is not an integer`);
|
|
171
|
+
}
|
|
172
|
+
if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
173
|
+
throw new Error(`${label} is outside the safe integer range`);
|
|
174
|
+
}
|
|
175
|
+
return Number(parsed);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function wireBrowserPlatform(platform: Os): "linux" | "macos" | "windows" {
|
|
179
|
+
switch (platform) {
|
|
180
|
+
case Os.OS_LINUX:
|
|
181
|
+
return "linux";
|
|
182
|
+
case Os.OS_MACOS:
|
|
183
|
+
return "macos";
|
|
184
|
+
case Os.OS_WINDOWS:
|
|
185
|
+
return "windows";
|
|
186
|
+
default:
|
|
187
|
+
throw new Error("attached browser platform is unspecified or unsupported");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function wireBrowserArchitecture(architecture: Arch): "x64" | "arm64" {
|
|
192
|
+
switch (architecture) {
|
|
193
|
+
case Arch.ARCH_X86_64:
|
|
194
|
+
return "x64";
|
|
195
|
+
case Arch.ARCH_AARCH64:
|
|
196
|
+
return "arm64";
|
|
197
|
+
default:
|
|
198
|
+
throw new Error("attached browser architecture is unspecified or unsupported");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function ingestAttachedBrowserInventory(
|
|
203
|
+
db: Database,
|
|
204
|
+
input: {
|
|
205
|
+
workspaceId: string;
|
|
206
|
+
agentId: string;
|
|
207
|
+
inventory: WireAttachedBrowserInventorySnapshot;
|
|
208
|
+
},
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const enrollment = await getEnrollment(db, input.workspaceId, input.agentId);
|
|
211
|
+
if (!enrollment) return;
|
|
212
|
+
await reconcileAttachedBrowserInventory(db, {
|
|
213
|
+
accountId: enrollment.accountId,
|
|
214
|
+
workspaceId: input.workspaceId,
|
|
215
|
+
enrollmentId: input.agentId,
|
|
216
|
+
snapshot: wireAttachedBrowserInventoryToContract(input.inventory),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
127
220
|
/**
|
|
128
221
|
* Ingest ONE decoded heartbeat for an enrolled machine. Resolves the enrollment's
|
|
129
222
|
* accountId (needed for the RLS-scoped writes) from the enrollment row; an
|
|
@@ -253,6 +346,18 @@ export async function handleAgentEventPayload(
|
|
|
253
346
|
enrollmentId: ids.agentId,
|
|
254
347
|
reason,
|
|
255
348
|
});
|
|
349
|
+
try {
|
|
350
|
+
await disconnectAttachedBrowserDevices(db, {
|
|
351
|
+
accountId: enrollment.accountId,
|
|
352
|
+
workspaceId: ids.workspaceId,
|
|
353
|
+
enrollmentId: ids.agentId,
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
observability?.warn?.("Failed to disconnect attached browsers with their machine", {
|
|
357
|
+
subject,
|
|
358
|
+
error: error instanceof Error ? error.message : String(error),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
256
361
|
// Fan out the link-plane events to the sessions with an active op on this
|
|
257
362
|
// machine: machine.link.lost (its control link is going away) for every
|
|
258
363
|
// clean going-offline, PLUS machine.runner.restarted when the reason is a
|
|
@@ -294,10 +399,23 @@ export async function handleAgentEventPayload(
|
|
|
294
399
|
if (event.event?.$case !== "heartbeat") {
|
|
295
400
|
return; // an unknown event kind → not a metrics point.
|
|
296
401
|
}
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
299
|
-
|
|
402
|
+
const heartbeat = event.event.heartbeat;
|
|
403
|
+
if (heartbeat.attachedBrowserInventory) {
|
|
404
|
+
try {
|
|
405
|
+
await ingestAttachedBrowserInventory(db, {
|
|
406
|
+
workspaceId: ids.workspaceId,
|
|
407
|
+
agentId: ids.agentId,
|
|
408
|
+
inventory: heartbeat.attachedBrowserInventory,
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
observability?.warn?.("Failed to ingest an attached-browser inventory", {
|
|
412
|
+
subject,
|
|
413
|
+
error: error instanceof Error ? error.message : String(error),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
300
416
|
}
|
|
417
|
+
const metrics = heartbeat.metrics;
|
|
418
|
+
if (!metrics) return;
|
|
301
419
|
try {
|
|
302
420
|
await ingestHeartbeat(db, {
|
|
303
421
|
workspaceId: ids.workspaceId,
|
|
@@ -21,8 +21,11 @@ import {
|
|
|
21
21
|
requirePersistableReplacementSandboxEnvelope,
|
|
22
22
|
resolveModalCheckpointProviderBindingForSession,
|
|
23
23
|
serializeReplacementSandboxEnvelope,
|
|
24
|
+
SandboxProviderContinuityUnavailableError,
|
|
24
25
|
tagModalSandbox,
|
|
26
|
+
terminateUnpublishedSandboxSession,
|
|
25
27
|
verifySandboxExecReadiness,
|
|
28
|
+
withoutSandboxProviderIdentity,
|
|
26
29
|
WorkspaceArchiveIntegrityError,
|
|
27
30
|
type EstablishedSandboxSession,
|
|
28
31
|
type WorkspaceArchiveDescriptor,
|
|
@@ -52,39 +55,10 @@ function legacyNativeArchiveFromEnvelope(envelope: Record<string, unknown> | nul
|
|
|
52
55
|
);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
function withoutProviderIdentity(
|
|
56
|
-
envelope: Record<string, unknown> | null,
|
|
57
|
-
): Record<string, unknown> | null {
|
|
58
|
-
if (!envelope) return null;
|
|
59
|
-
const sessionState =
|
|
60
|
-
envelope.sessionState && typeof envelope.sessionState === "object"
|
|
61
|
-
? (envelope.sessionState as Record<string, unknown>)
|
|
62
|
-
: null;
|
|
63
|
-
if (!sessionState) return envelope;
|
|
64
|
-
const { providerState: _providerState, ...providerIndependentState } = sessionState;
|
|
65
|
-
return { ...envelope, sessionState: providerIndependentState };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
async function terminateCreated(established: EstablishedSandboxSession | null): Promise<boolean> {
|
|
69
59
|
if (!established) return true;
|
|
70
|
-
const client = established.client as {
|
|
71
|
-
delete?: (state: unknown) => Promise<unknown>;
|
|
72
|
-
};
|
|
73
60
|
try {
|
|
74
|
-
|
|
75
|
-
await client.delete(established.sessionState);
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
const session = established.session as {
|
|
79
|
-
terminate?: () => Promise<unknown>;
|
|
80
|
-
kill?: () => Promise<unknown>;
|
|
81
|
-
close?: () => Promise<unknown>;
|
|
82
|
-
closed?: boolean;
|
|
83
|
-
};
|
|
84
|
-
if (session.terminate) await session.terminate();
|
|
85
|
-
else if (session.kill) await session.kill();
|
|
86
|
-
else if (session.close && !session.closed) await session.close();
|
|
87
|
-
else return false;
|
|
61
|
+
await terminateUnpublishedSandboxSession(established);
|
|
88
62
|
return true;
|
|
89
63
|
} catch (error) {
|
|
90
64
|
return isProviderSandboxNotFoundError(established.backendId, error);
|
|
@@ -112,7 +86,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
112
86
|
const fallbackArchiveEnvelope =
|
|
113
87
|
input.acquiredLease.recovery.archive.status === "none" &&
|
|
114
88
|
hasWorkspaceArchive(input.fallbackEnvelope)
|
|
115
|
-
?
|
|
89
|
+
? withoutSandboxProviderIdentity(input.fallbackEnvelope)
|
|
116
90
|
: null;
|
|
117
91
|
let spawnEnvelope =
|
|
118
92
|
fallbackArchiveEnvelope ?? input.acquiredLease.resumeState ?? input.fallbackEnvelope;
|
|
@@ -131,10 +105,12 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
131
105
|
ReturnType<typeof resolveModalCheckpointProviderBindingForSession>
|
|
132
106
|
> | null;
|
|
133
107
|
} | null = null;
|
|
108
|
+
const continuityRecovery = input.acquiredLease.recovery.continuity;
|
|
134
109
|
try {
|
|
135
110
|
if (
|
|
136
|
-
input.acquiredLease.recovery.archive.status === "available"
|
|
137
|
-
|
|
111
|
+
(input.acquiredLease.recovery.archive.status === "available" &&
|
|
112
|
+
input.acquiredLease.archiveComplete) ||
|
|
113
|
+
(input.acquiredLease.recovery.archive.status === "none" && hasWorkspaceArchive(archiveSource))
|
|
138
114
|
) {
|
|
139
115
|
const id = crypto.randomUUID();
|
|
140
116
|
const legacyNativeArchive = legacyNativeArchiveFromEnvelope(archiveSource);
|
|
@@ -177,7 +153,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
177
153
|
legacyCheckpoint: begun.checkpointArtifact === null ? legacyNativeArchive : null,
|
|
178
154
|
legacyProviderBinding: null,
|
|
179
155
|
};
|
|
180
|
-
} else if (input.acquiredLease.recovery.archive.status !== "none") {
|
|
156
|
+
} else if (input.acquiredLease.recovery.archive.status !== "none" && !continuityRecovery) {
|
|
181
157
|
throw new SandboxLeaseRecoveryBlockedError(
|
|
182
158
|
input.sandboxGroupId,
|
|
183
159
|
input.expectedEpoch,
|
|
@@ -235,6 +211,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
235
211
|
sandboxGroupId: input.sandboxGroupId,
|
|
236
212
|
expectedEpoch: input.expectedEpoch,
|
|
237
213
|
rematerializationId: rematerialization?.id ?? null,
|
|
214
|
+
...(created.providerContinuity ? { continuityRecovery: created.providerContinuity } : {}),
|
|
238
215
|
instanceId: created.instanceId,
|
|
239
216
|
resumeBackendId: created.backendId,
|
|
240
217
|
resumeState,
|
|
@@ -310,6 +287,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
310
287
|
await verifySandboxExecReadiness(established);
|
|
311
288
|
if (
|
|
312
289
|
rematerialization &&
|
|
290
|
+
!established.providerContinuity &&
|
|
313
291
|
established.restoredArchive?.revision !== rematerialization.selectedRevision
|
|
314
292
|
) {
|
|
315
293
|
throw new WorkspaceArchiveIntegrityError(
|
|
@@ -330,19 +308,21 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
330
308
|
dataPlaneUrl: input.dataPlaneUrl,
|
|
331
309
|
resumeBackendId: established.backendId,
|
|
332
310
|
resumeState,
|
|
333
|
-
...(
|
|
334
|
-
? {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
311
|
+
...(established.providerContinuity
|
|
312
|
+
? { continuityRecovery: established.providerContinuity }
|
|
313
|
+
: rematerialization
|
|
314
|
+
? {
|
|
315
|
+
rematerialization: {
|
|
316
|
+
id: rematerialization.id,
|
|
317
|
+
verifiedRevision: rematerialization.selectedRevision,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
: {}),
|
|
341
321
|
leaseTtlMs: input.settings.sandboxLeaseTtlMs,
|
|
342
322
|
});
|
|
343
323
|
if (!committed.committed || !committed.lease) {
|
|
344
324
|
const terminated = await terminateCreated(established);
|
|
345
|
-
if (terminated && rematerialization) {
|
|
325
|
+
if (terminated && rematerialization && !established.providerContinuity) {
|
|
346
326
|
await failSandboxRematerialization(input.db, {
|
|
347
327
|
accountId: input.accountId,
|
|
348
328
|
workspaceId: input.workspaceId,
|
|
@@ -367,18 +347,25 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
367
347
|
if (error instanceof SandboxLeaseSupersededError) throw error;
|
|
368
348
|
const terminated = await terminateCreated(established);
|
|
369
349
|
if (terminated) {
|
|
370
|
-
|
|
350
|
+
const continuityUnavailable = error instanceof SandboxProviderContinuityUnavailableError;
|
|
351
|
+
if (rematerialization && !established?.providerContinuity) {
|
|
371
352
|
await failSandboxRematerialization(input.db, {
|
|
372
353
|
accountId: input.accountId,
|
|
373
354
|
workspaceId: input.workspaceId,
|
|
374
355
|
sandboxGroupId: input.sandboxGroupId,
|
|
375
356
|
expectedEpoch: input.expectedEpoch,
|
|
376
357
|
rematerializationId: rematerialization.id,
|
|
377
|
-
failureCode:
|
|
378
|
-
|
|
358
|
+
failureCode: continuityUnavailable
|
|
359
|
+
? "provider_continuity_unavailable"
|
|
360
|
+
: error instanceof WorkspaceArchiveIntegrityError
|
|
379
361
|
? error.code
|
|
380
362
|
: "sandbox_rematerialization_failed",
|
|
381
|
-
retryable:
|
|
363
|
+
retryable: continuityUnavailable
|
|
364
|
+
? error.retryable
|
|
365
|
+
: error instanceof WorkspaceArchiveIntegrityError
|
|
366
|
+
? error.retryable
|
|
367
|
+
: true,
|
|
368
|
+
...(continuityUnavailable ? { discardContinuity: true } : {}),
|
|
382
369
|
});
|
|
383
370
|
} else {
|
|
384
371
|
await failWarmingToCold(input.db, {
|
|
@@ -386,6 +373,7 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
386
373
|
workspaceId: input.workspaceId,
|
|
387
374
|
sandboxGroupId: input.sandboxGroupId,
|
|
388
375
|
expectedEpoch: input.expectedEpoch,
|
|
376
|
+
...(continuityUnavailable ? { discardContinuity: true } : {}),
|
|
389
377
|
});
|
|
390
378
|
}
|
|
391
379
|
}
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// attach -> acquireLease(kind:'viewer') under FOR UPDATE + cold->warming CAS.
|
|
10
10
|
// spawner role -> establish the box in-process + commitWarmingToWarm.
|
|
11
11
|
// attached/rearmed -> the holder alone keeps the box warm.
|
|
12
|
-
//
|
|
12
|
+
// lifecycle claim -> wait for settlement; true epoch supersession
|
|
13
|
+
// -> release + surface a 409.
|
|
13
14
|
// heartbeat -> heartbeatLeaseHolder (epoch-fenced) refreshes the holder TTL.
|
|
14
15
|
// detach -> releaseLeaseHolder (idempotent); the reaper (P1.3) stop()s the
|
|
15
16
|
// box at refcount 0 past the drain grace.
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
hasGitCredentialRepositorySelection,
|
|
25
26
|
hasGitHubRepositorySelection,
|
|
26
27
|
resolveStreamTokenSecret,
|
|
27
|
-
|
|
28
|
+
sandboxLifecycleTransitionWaitMs,
|
|
28
29
|
stableSandboxEnvironmentForRun,
|
|
29
30
|
} from "@opengeni/config";
|
|
30
31
|
import type { Settings } from "@opengeni/config";
|
|
@@ -62,6 +63,8 @@ import {
|
|
|
62
63
|
exposeStreamPort,
|
|
63
64
|
desktopCapableBackend,
|
|
64
65
|
NatsControlRpc,
|
|
66
|
+
SandboxResumeIdentityMismatchError,
|
|
67
|
+
SandboxResumeIdentityUnavailableError,
|
|
65
68
|
SelfhostedSandboxClient,
|
|
66
69
|
TERMINAL_STREAM_PORT,
|
|
67
70
|
DisplayStackUnsupportedError,
|
|
@@ -74,6 +77,7 @@ import {
|
|
|
74
77
|
} from "@opengeni/runtime/sandbox";
|
|
75
78
|
import { relayConfigFromSettings } from "@opengeni/core";
|
|
76
79
|
import { establishApiSandboxSpawner } from "./rematerialize";
|
|
80
|
+
import { establishCachedChannelAHandle } from "./channel-a";
|
|
77
81
|
|
|
78
82
|
/** The minimal services a viewer op needs: the DB + settings (lease cadence +
|
|
79
83
|
* the sandbox client construction the leaf reads from settings). The bus is
|
|
@@ -178,7 +182,14 @@ export async function sessionAttachEnvironment(
|
|
|
178
182
|
*/
|
|
179
183
|
export async function attachViewer(
|
|
180
184
|
services: ViewerServices,
|
|
181
|
-
input: {
|
|
185
|
+
input: {
|
|
186
|
+
accountId: string;
|
|
187
|
+
workspaceId: string;
|
|
188
|
+
session: Session;
|
|
189
|
+
viewerId?: string;
|
|
190
|
+
/** Cancel lifecycle waiting when the originating HTTP request disconnects. */
|
|
191
|
+
waitSignal?: AbortSignal;
|
|
192
|
+
},
|
|
182
193
|
): Promise<ViewerAttachResult> {
|
|
183
194
|
const { db, settings } = services;
|
|
184
195
|
const { accountId, workspaceId, session } = input;
|
|
@@ -210,7 +221,8 @@ export async function attachViewer(
|
|
|
210
221
|
os: session.sandboxOs,
|
|
211
222
|
leaseTtlMs,
|
|
212
223
|
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
213
|
-
captureWaitMs:
|
|
224
|
+
captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
|
|
225
|
+
...(input.waitSignal ? { waitSignal: input.waitSignal } : {}),
|
|
214
226
|
});
|
|
215
227
|
} catch (error) {
|
|
216
228
|
if (error instanceof SandboxViewerAdmissionBlockedError) {
|
|
@@ -236,7 +248,10 @@ export async function attachViewer(
|
|
|
236
248
|
if (acquired.role === "fenced") {
|
|
237
249
|
await release();
|
|
238
250
|
throw new HTTPException(409, {
|
|
239
|
-
message:
|
|
251
|
+
message:
|
|
252
|
+
acquired.reason === "superseded"
|
|
253
|
+
? `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach`
|
|
254
|
+
: `sandbox lifecycle transition in progress (${acquired.reason}, epoch ${acquired.lease.leaseEpoch}, backend ${acquired.lease.backend}, instance ${acquired.lease.instanceId ?? "none"}); retry`,
|
|
240
255
|
});
|
|
241
256
|
}
|
|
242
257
|
|
|
@@ -361,6 +376,12 @@ export async function attachViewer(
|
|
|
361
376
|
message: `sandbox instance was lost; retry to restore it`,
|
|
362
377
|
});
|
|
363
378
|
}
|
|
379
|
+
if (
|
|
380
|
+
error instanceof SandboxResumeIdentityMismatchError ||
|
|
381
|
+
error instanceof SandboxResumeIdentityUnavailableError
|
|
382
|
+
) {
|
|
383
|
+
throw new HTTPException(409, { message: error.message });
|
|
384
|
+
}
|
|
364
385
|
throw error;
|
|
365
386
|
} finally {
|
|
366
387
|
await dropEstablishedHandle(observed);
|
|
@@ -624,7 +645,7 @@ export type MintDesktopStreamInput = {
|
|
|
624
645
|
session: Session;
|
|
625
646
|
/** The viewer holder id the scoped token is minted for. */
|
|
626
647
|
viewerId: string;
|
|
627
|
-
/** The live lease (must be warm
|
|
648
|
+
/** The live lease (must be holder-confirmed warm). A selfhosted-active
|
|
628
649
|
* session may have no Modal group lease; omit and the selfhosted branch handles it. */
|
|
629
650
|
lease?: LeaseSnapshot;
|
|
630
651
|
/** The epoch the CALLER last observed the URL minted under. When the live
|
|
@@ -716,7 +737,7 @@ export async function mintDesktopStream(
|
|
|
716
737
|
|
|
717
738
|
// GATE 2: the box must be live (the handshake never spins one up — a cold box
|
|
718
739
|
// returns lease_cold; the viewer-attach path warms it first, then mints).
|
|
719
|
-
if (!lease ||
|
|
740
|
+
if (!lease || lease.liveness !== "warm") {
|
|
720
741
|
return null;
|
|
721
742
|
}
|
|
722
743
|
|
|
@@ -730,18 +751,22 @@ export async function mintDesktopStream(
|
|
|
730
751
|
// SAME stable run-env the turn declares, so a later turn finds no env delta.
|
|
731
752
|
const environment = await sessionAttachEnvironment(services, workspaceId, session);
|
|
732
753
|
try {
|
|
754
|
+
const establish = () =>
|
|
755
|
+
(services.establishSandboxSession ?? establishSandboxSessionFromEnvelope)(
|
|
756
|
+
settings,
|
|
757
|
+
envelope,
|
|
758
|
+
{
|
|
759
|
+
sessionId: session.id,
|
|
760
|
+
recovery: "resume-only",
|
|
761
|
+
backendOverride: session.sandboxBackend,
|
|
762
|
+
environment,
|
|
763
|
+
},
|
|
764
|
+
);
|
|
733
765
|
established = input.establish
|
|
734
766
|
? await input.establish(envelope)
|
|
735
|
-
:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
{
|
|
739
|
-
sessionId: session.id,
|
|
740
|
-
recovery: "resume-only",
|
|
741
|
-
backendOverride: session.sandboxBackend,
|
|
742
|
-
environment,
|
|
743
|
-
},
|
|
744
|
-
);
|
|
767
|
+
: services.establishSandboxSession
|
|
768
|
+
? await establish()
|
|
769
|
+
: await establishCachedChannelAHandle(workspaceId, session.id, lease, establish);
|
|
745
770
|
} catch (error) {
|
|
746
771
|
await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
|
|
747
772
|
return null;
|
|
@@ -874,7 +899,7 @@ export type MintTerminalStreamInput = {
|
|
|
874
899
|
session: Session;
|
|
875
900
|
/** The viewer holder / principal id the scoped token is minted for. */
|
|
876
901
|
viewerId: string;
|
|
877
|
-
/** The live lease (must be warm
|
|
902
|
+
/** The live lease (must be holder-confirmed warm). A selfhosted-active
|
|
878
903
|
* session may have no Modal group lease; omit and the selfhosted branch handles it. */
|
|
879
904
|
lease?: LeaseSnapshot;
|
|
880
905
|
/** Test seam: override how the box is re-established by id (see
|
|
@@ -942,7 +967,7 @@ export async function mintTerminalStream(
|
|
|
942
967
|
}
|
|
943
968
|
|
|
944
969
|
// GATE 2: the box must be live (the handshake never spins one up).
|
|
945
|
-
if (!lease ||
|
|
970
|
+
if (!lease || lease.liveness !== "warm") {
|
|
946
971
|
return null;
|
|
947
972
|
}
|
|
948
973
|
|
|
@@ -955,18 +980,22 @@ export async function mintTerminalStream(
|
|
|
955
980
|
// declares, so a later turn finds no manifest-env delta.
|
|
956
981
|
const environment = await sessionAttachEnvironment(services, workspaceId, session);
|
|
957
982
|
try {
|
|
983
|
+
const establish = () =>
|
|
984
|
+
(services.establishSandboxSession ?? establishSandboxSessionFromEnvelope)(
|
|
985
|
+
settings,
|
|
986
|
+
envelope,
|
|
987
|
+
{
|
|
988
|
+
sessionId: session.id,
|
|
989
|
+
recovery: "resume-only",
|
|
990
|
+
backendOverride: session.sandboxBackend,
|
|
991
|
+
environment,
|
|
992
|
+
},
|
|
993
|
+
);
|
|
958
994
|
established = input.establish
|
|
959
995
|
? await input.establish(envelope)
|
|
960
|
-
:
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
{
|
|
964
|
-
sessionId: session.id,
|
|
965
|
-
recovery: "resume-only",
|
|
966
|
-
backendOverride: session.sandboxBackend,
|
|
967
|
-
environment,
|
|
968
|
-
},
|
|
969
|
-
);
|
|
996
|
+
: services.establishSandboxSession
|
|
997
|
+
? await establish()
|
|
998
|
+
: await establishCachedChannelAHandle(workspaceId, session.id, lease, establish);
|
|
970
999
|
} catch (error) {
|
|
971
1000
|
await retireMissingWarmLease(services, { accountId, workspaceId, session, lease }, error);
|
|
972
1001
|
return null;
|