@opengeni/api-router 0.23.1 → 0.30.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/app.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -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/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- 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/fiken.d.ts +233 -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 +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -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/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -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-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -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 +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
package/src/sandbox/machines.ts
CHANGED
|
@@ -156,8 +156,8 @@ function machineStateFor(
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* Build the Machines dashboard response for a workspace. When `sessionId` is
|
|
159
|
-
* supplied (an in-session view) the session's synthetic
|
|
160
|
-
* prepended (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
|
|
159
|
+
* supplied (an in-session view) the session's synthetic home group box is
|
|
160
|
+
* prepended when one exists (`isSessionGroup:true`) and the active-sandbox pointer is echoed;
|
|
161
161
|
* without it (the pure workspace dashboard) `activeSandboxId` is null and only
|
|
162
162
|
* the enrolled machines are listed.
|
|
163
163
|
*/
|
|
@@ -184,9 +184,9 @@ export async function listMachines(
|
|
|
184
184
|
|
|
185
185
|
const machines: MachineView[] = [];
|
|
186
186
|
|
|
187
|
-
// The session's own
|
|
188
|
-
//
|
|
189
|
-
if (session) {
|
|
187
|
+
// The session's own group box (synthetic): the default/home sandbox a null
|
|
188
|
+
// active pointer routes to. A backend:none session has no home box.
|
|
189
|
+
if (session && session.sandboxBackend !== "none") {
|
|
190
190
|
const groupActive = activeSandboxId === null;
|
|
191
191
|
const groupLease = await readLease(db, workspaceId, session.sandboxGroupId);
|
|
192
192
|
machines.push(
|
|
@@ -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,
|
|
@@ -179,7 +182,14 @@ export async function sessionAttachEnvironment(
|
|
|
179
182
|
*/
|
|
180
183
|
export async function attachViewer(
|
|
181
184
|
services: ViewerServices,
|
|
182
|
-
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
|
+
},
|
|
183
193
|
): Promise<ViewerAttachResult> {
|
|
184
194
|
const { db, settings } = services;
|
|
185
195
|
const { accountId, workspaceId, session } = input;
|
|
@@ -211,7 +221,8 @@ export async function attachViewer(
|
|
|
211
221
|
os: session.sandboxOs,
|
|
212
222
|
leaseTtlMs,
|
|
213
223
|
warmingLeaseTtlMs: settings.sandboxWarmingTimeoutMs,
|
|
214
|
-
captureWaitMs:
|
|
224
|
+
captureWaitMs: sandboxLifecycleTransitionWaitMs(settings),
|
|
225
|
+
...(input.waitSignal ? { waitSignal: input.waitSignal } : {}),
|
|
215
226
|
});
|
|
216
227
|
} catch (error) {
|
|
217
228
|
if (error instanceof SandboxViewerAdmissionBlockedError) {
|
|
@@ -237,7 +248,10 @@ export async function attachViewer(
|
|
|
237
248
|
if (acquired.role === "fenced") {
|
|
238
249
|
await release();
|
|
239
250
|
throw new HTTPException(409, {
|
|
240
|
-
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`,
|
|
241
255
|
});
|
|
242
256
|
}
|
|
243
257
|
|
|
@@ -362,6 +376,12 @@ export async function attachViewer(
|
|
|
362
376
|
message: `sandbox instance was lost; retry to restore it`,
|
|
363
377
|
});
|
|
364
378
|
}
|
|
379
|
+
if (
|
|
380
|
+
error instanceof SandboxResumeIdentityMismatchError ||
|
|
381
|
+
error instanceof SandboxResumeIdentityUnavailableError
|
|
382
|
+
) {
|
|
383
|
+
throw new HTTPException(409, { message: error.message });
|
|
384
|
+
}
|
|
365
385
|
throw error;
|
|
366
386
|
} finally {
|
|
367
387
|
await dropEstablishedHandle(observed);
|
|
@@ -625,7 +645,7 @@ export type MintDesktopStreamInput = {
|
|
|
625
645
|
session: Session;
|
|
626
646
|
/** The viewer holder id the scoped token is minted for. */
|
|
627
647
|
viewerId: string;
|
|
628
|
-
/** The live lease (must be warm
|
|
648
|
+
/** The live lease (must be holder-confirmed warm). A selfhosted-active
|
|
629
649
|
* session may have no Modal group lease; omit and the selfhosted branch handles it. */
|
|
630
650
|
lease?: LeaseSnapshot;
|
|
631
651
|
/** The epoch the CALLER last observed the URL minted under. When the live
|
|
@@ -717,7 +737,7 @@ export async function mintDesktopStream(
|
|
|
717
737
|
|
|
718
738
|
// GATE 2: the box must be live (the handshake never spins one up — a cold box
|
|
719
739
|
// returns lease_cold; the viewer-attach path warms it first, then mints).
|
|
720
|
-
if (!lease ||
|
|
740
|
+
if (!lease || lease.liveness !== "warm") {
|
|
721
741
|
return null;
|
|
722
742
|
}
|
|
723
743
|
|
|
@@ -871,15 +891,18 @@ export type TerminalStreamMint = {
|
|
|
871
891
|
token: string;
|
|
872
892
|
expiresAt: string;
|
|
873
893
|
leaseEpoch: number;
|
|
894
|
+
transport: "pty-ws" | "relay-pty";
|
|
874
895
|
};
|
|
875
896
|
|
|
897
|
+
type SelfhostedStreamMint = Omit<TerminalStreamMint, "transport">;
|
|
898
|
+
|
|
876
899
|
export type MintTerminalStreamInput = {
|
|
877
900
|
accountId: string;
|
|
878
901
|
workspaceId: string;
|
|
879
902
|
session: Session;
|
|
880
903
|
/** The viewer holder / principal id the scoped token is minted for. */
|
|
881
904
|
viewerId: string;
|
|
882
|
-
/** The live lease (must be warm
|
|
905
|
+
/** The live lease (must be holder-confirmed warm). A selfhosted-active
|
|
883
906
|
* session may have no Modal group lease; omit and the selfhosted branch handles it. */
|
|
884
907
|
lease?: LeaseSnapshot;
|
|
885
908
|
/** Test seam: override how the box is re-established by id (see
|
|
@@ -930,7 +953,7 @@ export async function mintTerminalStream(
|
|
|
930
953
|
if (session.activeSandboxId) {
|
|
931
954
|
const active = await getSandbox(db, workspaceId, session.activeSandboxId);
|
|
932
955
|
if (active?.kind === "selfhosted") {
|
|
933
|
-
|
|
956
|
+
const stream = await tryMintActiveSelfhostedStream(
|
|
934
957
|
services,
|
|
935
958
|
{
|
|
936
959
|
session,
|
|
@@ -941,13 +964,14 @@ export async function mintTerminalStream(
|
|
|
941
964
|
},
|
|
942
965
|
input.resolveSelfhostedSession,
|
|
943
966
|
);
|
|
967
|
+
return stream ? { ...stream, transport: "relay-pty" } : null;
|
|
944
968
|
}
|
|
945
969
|
// A Modal swap target (or unknown) falls through to the existing group-box path
|
|
946
970
|
// (unchanged — Modal swap-target streaming is out of scope for this fix).
|
|
947
971
|
}
|
|
948
972
|
|
|
949
973
|
// GATE 2: the box must be live (the handshake never spins one up).
|
|
950
|
-
if (!lease ||
|
|
974
|
+
if (!lease || lease.liveness !== "warm") {
|
|
951
975
|
return null;
|
|
952
976
|
}
|
|
953
977
|
|
|
@@ -1032,6 +1056,7 @@ export async function mintTerminalStream(
|
|
|
1032
1056
|
token: exposed.token,
|
|
1033
1057
|
expiresAt: exposed.expiresAt,
|
|
1034
1058
|
leaseEpoch: lease.leaseEpoch,
|
|
1059
|
+
transport: "pty-ws",
|
|
1035
1060
|
};
|
|
1036
1061
|
} catch {
|
|
1037
1062
|
// Any other failure degrades the terminal pty-ws cell to the sse-events
|
|
@@ -1087,7 +1112,7 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1087
1112
|
resolveSelfhostedSession?: (
|
|
1088
1113
|
sandbox: SandboxRecord,
|
|
1089
1114
|
) => Promise<{ resolveExposedPort?: (port: number) => Promise<unknown> }>,
|
|
1090
|
-
): Promise<
|
|
1115
|
+
): Promise<SelfhostedStreamMint | null> {
|
|
1091
1116
|
const { settings, bus } = services;
|
|
1092
1117
|
const { session, workspaceId, port, sandbox } = input;
|
|
1093
1118
|
if (!sandbox.enrollmentId) {
|
|
@@ -1108,6 +1133,8 @@ async function tryMintActiveSelfhostedStream(
|
|
|
1108
1133
|
controlRpcFactory: () => controlRpc(bus),
|
|
1109
1134
|
agentId: sandbox.enrollmentId,
|
|
1110
1135
|
epoch: session.activeEpoch,
|
|
1136
|
+
// Stream authority rotates; terminal process identity does not.
|
|
1137
|
+
terminalScopeId: session.id,
|
|
1111
1138
|
});
|
|
1112
1139
|
shSession = await client.resume({ agentId: sandbox.enrollmentId });
|
|
1113
1140
|
}
|
|
@@ -1165,7 +1192,7 @@ export type MintSelfhostedStreamInput = {
|
|
|
1165
1192
|
export async function mintSelfhostedStream(
|
|
1166
1193
|
services: ViewerServices,
|
|
1167
1194
|
input: MintSelfhostedStreamInput,
|
|
1168
|
-
): Promise<
|
|
1195
|
+
): Promise<SelfhostedStreamMint | null> {
|
|
1169
1196
|
const { settings } = services;
|
|
1170
1197
|
const secret = resolveStreamTokenSecret(settings);
|
|
1171
1198
|
if (!secret) {
|