@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/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SlackBotProviderError,
|
|
2
3
|
createAppComposition,
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
createOpenGeniSlackBotInteractionClient,
|
|
5
|
+
startSlackInteractionPump,
|
|
6
|
+
startTemporalScheduleCleanupPump
|
|
7
|
+
} from "./chunk-2PQMSRCB.js";
|
|
5
8
|
|
|
6
9
|
// src/index.ts
|
|
7
10
|
import {
|
|
@@ -21,6 +24,8 @@ import {
|
|
|
21
24
|
} from "@opengeni/db";
|
|
22
25
|
import { createNatsEventBus } from "@opengeni/events";
|
|
23
26
|
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
27
|
+
import { createObjectStorage } from "@opengeni/storage";
|
|
28
|
+
import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
|
|
24
29
|
import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
|
|
25
30
|
import {
|
|
26
31
|
Connection,
|
|
@@ -164,19 +169,26 @@ async function startAuthCalloutResponder(deps, natsUrl) {
|
|
|
164
169
|
// src/sandbox/metrics-ingestion.ts
|
|
165
170
|
import {
|
|
166
171
|
clearEnrollmentWentOffline,
|
|
172
|
+
disconnectAttachedBrowserDevices,
|
|
167
173
|
getEnrollment as getEnrollment2,
|
|
168
174
|
ingestMachineMetricsSample,
|
|
175
|
+
reconcileAttachedBrowserInventory,
|
|
169
176
|
sessionsWithActiveOpOnEnrollment,
|
|
170
177
|
setEnrollmentDisplayState,
|
|
171
178
|
setEnrollmentOpStreamState,
|
|
172
179
|
setEnrollmentWentOffline,
|
|
173
180
|
touchEnrollmentLastSeen
|
|
174
181
|
} from "@opengeni/db";
|
|
182
|
+
import {
|
|
183
|
+
AttachedBrowserInventorySnapshot as AttachedBrowserInventoryContract
|
|
184
|
+
} from "@opengeni/contracts";
|
|
175
185
|
import { appendAndPublishEvents } from "@opengeni/events";
|
|
176
186
|
import {
|
|
177
187
|
AgentEvent,
|
|
188
|
+
Arch,
|
|
178
189
|
GoingOfflineReason,
|
|
179
190
|
Hello,
|
|
191
|
+
Os,
|
|
180
192
|
goingOfflineReasonToJSON
|
|
181
193
|
} from "@opengeni/agent-proto";
|
|
182
194
|
var AGENT_EVENTS_SUBJECT = "agent.*.*.events";
|
|
@@ -215,6 +227,73 @@ function wireSampleToDbSample(wire) {
|
|
|
215
227
|
sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : /* @__PURE__ */ new Date()
|
|
216
228
|
};
|
|
217
229
|
}
|
|
230
|
+
function wireAttachedBrowserInventoryToContract(wire) {
|
|
231
|
+
return AttachedBrowserInventoryContract.parse({
|
|
232
|
+
bridgeGeneration: wire.bridgeGeneration,
|
|
233
|
+
revision: safeWireInteger(wire.revision, "browser inventory revision"),
|
|
234
|
+
devices: wire.devices.map((device) => ({
|
|
235
|
+
id: device.id,
|
|
236
|
+
name: device.name,
|
|
237
|
+
profileLabel: device.profileLabel.trim() || null,
|
|
238
|
+
browserName: device.browserName,
|
|
239
|
+
browserVersion: device.browserVersion,
|
|
240
|
+
extensionVersion: device.extensionVersion,
|
|
241
|
+
platform: wireBrowserPlatform(device.platform),
|
|
242
|
+
architecture: wireBrowserArchitecture(device.arch),
|
|
243
|
+
connectionGeneration: device.connectionGeneration,
|
|
244
|
+
inventoryRevision: safeWireInteger(
|
|
245
|
+
device.inventoryRevision,
|
|
246
|
+
"attached browser inventory revision"
|
|
247
|
+
),
|
|
248
|
+
tabCount: safeWireInteger(device.tabCount, "attached browser tab count"),
|
|
249
|
+
capabilities: device.capabilities
|
|
250
|
+
}))
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function safeWireInteger(value, label) {
|
|
254
|
+
let parsed;
|
|
255
|
+
try {
|
|
256
|
+
parsed = BigInt(value);
|
|
257
|
+
} catch {
|
|
258
|
+
throw new Error(`${label} is not an integer`);
|
|
259
|
+
}
|
|
260
|
+
if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
261
|
+
throw new Error(`${label} is outside the safe integer range`);
|
|
262
|
+
}
|
|
263
|
+
return Number(parsed);
|
|
264
|
+
}
|
|
265
|
+
function wireBrowserPlatform(platform) {
|
|
266
|
+
switch (platform) {
|
|
267
|
+
case Os.OS_LINUX:
|
|
268
|
+
return "linux";
|
|
269
|
+
case Os.OS_MACOS:
|
|
270
|
+
return "macos";
|
|
271
|
+
case Os.OS_WINDOWS:
|
|
272
|
+
return "windows";
|
|
273
|
+
default:
|
|
274
|
+
throw new Error("attached browser platform is unspecified or unsupported");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function wireBrowserArchitecture(architecture) {
|
|
278
|
+
switch (architecture) {
|
|
279
|
+
case Arch.ARCH_X86_64:
|
|
280
|
+
return "x64";
|
|
281
|
+
case Arch.ARCH_AARCH64:
|
|
282
|
+
return "arm64";
|
|
283
|
+
default:
|
|
284
|
+
throw new Error("attached browser architecture is unspecified or unsupported");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function ingestAttachedBrowserInventory(db, input) {
|
|
288
|
+
const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
|
|
289
|
+
if (!enrollment) return;
|
|
290
|
+
await reconcileAttachedBrowserInventory(db, {
|
|
291
|
+
accountId: enrollment.accountId,
|
|
292
|
+
workspaceId: input.workspaceId,
|
|
293
|
+
enrollmentId: input.agentId,
|
|
294
|
+
snapshot: wireAttachedBrowserInventoryToContract(input.inventory)
|
|
295
|
+
});
|
|
296
|
+
}
|
|
218
297
|
async function ingestHeartbeat(db, input) {
|
|
219
298
|
const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
|
|
220
299
|
if (!enrollment) {
|
|
@@ -285,6 +364,18 @@ async function handleAgentEventPayload(db, observability, payload, subject, bus)
|
|
|
285
364
|
enrollmentId: ids.agentId,
|
|
286
365
|
reason
|
|
287
366
|
});
|
|
367
|
+
try {
|
|
368
|
+
await disconnectAttachedBrowserDevices(db, {
|
|
369
|
+
accountId: enrollment.accountId,
|
|
370
|
+
workspaceId: ids.workspaceId,
|
|
371
|
+
enrollmentId: ids.agentId
|
|
372
|
+
});
|
|
373
|
+
} catch (error) {
|
|
374
|
+
observability?.warn?.("Failed to disconnect attached browsers with their machine", {
|
|
375
|
+
subject,
|
|
376
|
+
error: error instanceof Error ? error.message : String(error)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
288
379
|
if (bus) {
|
|
289
380
|
const isSelfUpdate = event.event.goingOffline.reason === GoingOfflineReason.GOING_OFFLINE_REASON_UPDATE;
|
|
290
381
|
await fanOutMachineLinkEvents(
|
|
@@ -320,10 +411,23 @@ async function handleAgentEventPayload(db, observability, payload, subject, bus)
|
|
|
320
411
|
if (event.event?.$case !== "heartbeat") {
|
|
321
412
|
return;
|
|
322
413
|
}
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
414
|
+
const heartbeat = event.event.heartbeat;
|
|
415
|
+
if (heartbeat.attachedBrowserInventory) {
|
|
416
|
+
try {
|
|
417
|
+
await ingestAttachedBrowserInventory(db, {
|
|
418
|
+
workspaceId: ids.workspaceId,
|
|
419
|
+
agentId: ids.agentId,
|
|
420
|
+
inventory: heartbeat.attachedBrowserInventory
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
observability?.warn?.("Failed to ingest an attached-browser inventory", {
|
|
424
|
+
subject,
|
|
425
|
+
error: error instanceof Error ? error.message : String(error)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
326
428
|
}
|
|
429
|
+
const metrics = heartbeat.metrics;
|
|
430
|
+
if (!metrics) return;
|
|
327
431
|
try {
|
|
328
432
|
await ingestHeartbeat(db, {
|
|
329
433
|
workspaceId: ids.workspaceId,
|
|
@@ -458,6 +562,1592 @@ function startHelloIngestion(deps) {
|
|
|
458
562
|
);
|
|
459
563
|
}
|
|
460
564
|
|
|
565
|
+
// src/memory-slack-delivery.ts
|
|
566
|
+
import {
|
|
567
|
+
claimMemorySlackPublication,
|
|
568
|
+
completeMemorySlackPublication,
|
|
569
|
+
currentMemorySlackConfigurationMatches,
|
|
570
|
+
deferMemorySlackPublication,
|
|
571
|
+
failMemorySlackPublication
|
|
572
|
+
} from "@opengeni/db";
|
|
573
|
+
import { sanitizeSlackPublicationText } from "@opengeni/core";
|
|
574
|
+
var DELIVERY_LEASE_MS = 3e4;
|
|
575
|
+
var MAX_DELIVERY_ATTEMPTS = 8;
|
|
576
|
+
var MAX_DELIVERY_RETRY_MS = 5 * 6e4;
|
|
577
|
+
var MAX_SLACK_MESSAGE_CHARS = 3500;
|
|
578
|
+
async function drainMemorySlackPublicationsOnce(deps) {
|
|
579
|
+
const claimHolderId = crypto.randomUUID();
|
|
580
|
+
const publication = await claimMemorySlackPublication(deps.db, claimHolderId, DELIVERY_LEASE_MS);
|
|
581
|
+
if (!publication) return false;
|
|
582
|
+
try {
|
|
583
|
+
if (!await currentMemorySlackConfigurationMatches(deps.db, publication)) {
|
|
584
|
+
await failMemorySlackPublication(deps.db, {
|
|
585
|
+
publication,
|
|
586
|
+
claimHolderId,
|
|
587
|
+
errorCode: "configuration_changed",
|
|
588
|
+
cancelled: true
|
|
589
|
+
});
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
const client = await createOpenGeniSlackBotInteractionClient(deps, {
|
|
593
|
+
accountId: publication.accountId,
|
|
594
|
+
workspaceId: publication.workspaceId,
|
|
595
|
+
connectionId: publication.connectionId,
|
|
596
|
+
subjectId: publication.initiatorSubjectId,
|
|
597
|
+
sessionId: publication.sessionId
|
|
598
|
+
});
|
|
599
|
+
const result = await client.postMessage({
|
|
600
|
+
operationId: publication.operationId,
|
|
601
|
+
channelId: publication.slackChannelId,
|
|
602
|
+
text: formatMemorySlackPublicationMessage(deps, publication),
|
|
603
|
+
requireActiveNonSharedChannel: true
|
|
604
|
+
});
|
|
605
|
+
const completed = await completeMemorySlackPublication(deps.db, {
|
|
606
|
+
publication,
|
|
607
|
+
claimHolderId,
|
|
608
|
+
slackChannelId: result.channelId,
|
|
609
|
+
slackMessageTimestamp: result.timestamp
|
|
610
|
+
});
|
|
611
|
+
if (!completed) throw new Error("Memory Slack publication lost its durable claim");
|
|
612
|
+
} catch (error) {
|
|
613
|
+
const errorCode = deliveryErrorCode(error);
|
|
614
|
+
if (memorySlackDestinationDrift(error)) {
|
|
615
|
+
await failMemorySlackPublication(deps.db, {
|
|
616
|
+
publication,
|
|
617
|
+
claimHolderId,
|
|
618
|
+
errorCode,
|
|
619
|
+
cancelled: true
|
|
620
|
+
}).catch(() => void 0);
|
|
621
|
+
} else if (publication.attemptCount >= MAX_DELIVERY_ATTEMPTS || permanentDeliveryError(error)) {
|
|
622
|
+
await failMemorySlackPublication(deps.db, {
|
|
623
|
+
publication,
|
|
624
|
+
claimHolderId,
|
|
625
|
+
errorCode
|
|
626
|
+
}).catch(() => void 0);
|
|
627
|
+
} else {
|
|
628
|
+
await deferMemorySlackPublication(deps.db, {
|
|
629
|
+
publication,
|
|
630
|
+
claimHolderId,
|
|
631
|
+
errorCode,
|
|
632
|
+
retryAt: new Date(Date.now() + deliveryRetryMs(error, publication.attemptCount))
|
|
633
|
+
}).catch(() => void 0);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
function startMemorySlackPublicationPump(deps, options = {}) {
|
|
639
|
+
let stopped = false;
|
|
640
|
+
let running;
|
|
641
|
+
const intervalMs = Math.max(250, options.intervalMs ?? 1e3);
|
|
642
|
+
const maxPerTick = Math.max(1, Math.min(50, options.maxPerTick ?? 10));
|
|
643
|
+
const tick = () => {
|
|
644
|
+
if (stopped || running) return;
|
|
645
|
+
running = (async () => {
|
|
646
|
+
for (let index = 0; index < maxPerTick; index += 1) {
|
|
647
|
+
if (!await drainMemorySlackPublicationsOnce(deps)) break;
|
|
648
|
+
}
|
|
649
|
+
})().catch((error) => {
|
|
650
|
+
deps.observability?.error("Memory Slack publication claim failed", {
|
|
651
|
+
errorClass: error instanceof Error ? error.name : "MemorySlackPublicationPumpError",
|
|
652
|
+
errorCode: deliveryErrorCode(error),
|
|
653
|
+
origin: "api"
|
|
654
|
+
});
|
|
655
|
+
}).finally(() => {
|
|
656
|
+
running = void 0;
|
|
657
|
+
});
|
|
658
|
+
};
|
|
659
|
+
const timer = setInterval(tick, intervalMs);
|
|
660
|
+
queueMicrotask(tick);
|
|
661
|
+
return async () => {
|
|
662
|
+
stopped = true;
|
|
663
|
+
clearInterval(timer);
|
|
664
|
+
await running;
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function formatMemorySlackPublicationMessage(deps, publication) {
|
|
668
|
+
const projection = publication.projection;
|
|
669
|
+
const summary = boundedProjectionString(projection["summary"], 512) || "Important change";
|
|
670
|
+
const owner = boundedProjectionString(projection["ownerLabel"], 96);
|
|
671
|
+
const occurredAt = boundedProjectionString(projection["occurredAt"], 64);
|
|
672
|
+
const changeKind = boundedProjectionString(projection["changeKind"], 32);
|
|
673
|
+
const destination = boundedProjectionString(projection["destination"], 64);
|
|
674
|
+
const outcome = boundedProjectionString(projection["outcome"], 64);
|
|
675
|
+
const heading = publication.sourceType === "workspace_memory" ? `Workspace Memory ${changeKind || "change"}` : `Governed learning ${outcome || "change"}`;
|
|
676
|
+
const details = [
|
|
677
|
+
`Importance: ${publication.importance}`,
|
|
678
|
+
...destination ? [`Surface: ${destination}`] : [],
|
|
679
|
+
...owner ? [`Owner: ${owner}`] : [],
|
|
680
|
+
...occurredAt ? [`Recorded: ${occurredAt}`] : []
|
|
681
|
+
];
|
|
682
|
+
const path = publication.sourceType === "workspace_memory" ? `/workspaces/${publication.workspaceId}/memory?memoryId=${encodeURIComponent(publication.sourceId)}` : `/workspaces/${publication.workspaceId}/workspace-state`;
|
|
683
|
+
const base = deps.settings.webBaseUrl ?? deps.settings.publicBaseUrl;
|
|
684
|
+
const link = base ? new URL(path, base).toString() : null;
|
|
685
|
+
const text = `*${heading}*
|
|
686
|
+
${summary}
|
|
687
|
+
|
|
688
|
+
${details.join(" \xB7 ")}${link ? `
|
|
689
|
+
${link}` : ""}`;
|
|
690
|
+
return text.length <= MAX_SLACK_MESSAGE_CHARS ? text : `${text.slice(0, MAX_SLACK_MESSAGE_CHARS - 20)}
|
|
691
|
+
\u2026 message truncated`;
|
|
692
|
+
}
|
|
693
|
+
var PERMANENT_DELIVERY_CODES = /* @__PURE__ */ new Set([
|
|
694
|
+
"account_inactive",
|
|
695
|
+
"channel_not_found",
|
|
696
|
+
"invalid_auth",
|
|
697
|
+
"is_archived",
|
|
698
|
+
"not_authed",
|
|
699
|
+
"not_in_channel",
|
|
700
|
+
"token_expired",
|
|
701
|
+
"token_revoked"
|
|
702
|
+
]);
|
|
703
|
+
var MEMORY_SLACK_DESTINATION_DRIFT_CODES = /* @__PURE__ */ new Set([
|
|
704
|
+
"channel_not_found",
|
|
705
|
+
"is_archived",
|
|
706
|
+
"not_in_channel",
|
|
707
|
+
"slack_connect_unsupported"
|
|
708
|
+
]);
|
|
709
|
+
function memorySlackDestinationDrift(error) {
|
|
710
|
+
return error instanceof SlackBotProviderError && MEMORY_SLACK_DESTINATION_DRIFT_CODES.has(error.code);
|
|
711
|
+
}
|
|
712
|
+
function permanentDeliveryError(error) {
|
|
713
|
+
if (!(error instanceof SlackBotProviderError)) return false;
|
|
714
|
+
if (PERMANENT_DELIVERY_CODES.has(error.code)) return true;
|
|
715
|
+
const status = /^http_(\d{3})$/.exec(error.code)?.[1];
|
|
716
|
+
return status ? Number(status) >= 400 && Number(status) < 500 && status !== "408" && status !== "429" : false;
|
|
717
|
+
}
|
|
718
|
+
function deliveryRetryMs(error, attemptCount) {
|
|
719
|
+
if (error instanceof SlackBotProviderError && error.retryAfterMs) {
|
|
720
|
+
return Math.min(Math.max(error.retryAfterMs, 1e3), MAX_DELIVERY_RETRY_MS);
|
|
721
|
+
}
|
|
722
|
+
return Math.min(1e3 * 2 ** Math.max(0, attemptCount - 1), MAX_DELIVERY_RETRY_MS);
|
|
723
|
+
}
|
|
724
|
+
function deliveryErrorCode(error) {
|
|
725
|
+
if (error instanceof SlackBotProviderError) return error.code.slice(0, 128);
|
|
726
|
+
const raw = error instanceof Error ? error.name : "memory_slack_delivery_error";
|
|
727
|
+
return raw.toLowerCase().replace(/[^a-z0-9_-]/g, "_").slice(0, 128) || "error";
|
|
728
|
+
}
|
|
729
|
+
function boundedProjectionString(value, maxChars) {
|
|
730
|
+
return typeof value === "string" ? escapeSlackText(sanitizeSlackPublicationText(value).slice(0, maxChars)) : "";
|
|
731
|
+
}
|
|
732
|
+
function escapeSlackText(value) {
|
|
733
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/editable-artifact-websocket.ts
|
|
737
|
+
import {
|
|
738
|
+
EditableArtifactDomainError,
|
|
739
|
+
EditableArtifactLiveError,
|
|
740
|
+
decodeEditableArtifactLiveClientWireFrame,
|
|
741
|
+
encodeEditableArtifactLiveServerWireFrame
|
|
742
|
+
} from "@opengeni/core";
|
|
743
|
+
var EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH = "/v1/editable-artifacts/live";
|
|
744
|
+
var EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL = "opengeni-artifact-v1";
|
|
745
|
+
var EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES = 8 * 1024 * 1024 + 64 * 1024;
|
|
746
|
+
var MAX_QUEUED_MESSAGES = 64;
|
|
747
|
+
var MAX_QUEUED_BYTES = 12 * 1024 * 1024;
|
|
748
|
+
var EditableArtifactWebSocketTransport = class {
|
|
749
|
+
constructor(application) {
|
|
750
|
+
this.application = application;
|
|
751
|
+
this.websocket = Object.freeze({
|
|
752
|
+
open: (socket) => socket.data.attach(socket),
|
|
753
|
+
message: (socket, message) => socket.data.receive(message),
|
|
754
|
+
close: (socket) => socket.data.transportClosed()
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
websocket;
|
|
758
|
+
handles(request) {
|
|
759
|
+
return new URL(request.url).pathname === EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH;
|
|
760
|
+
}
|
|
761
|
+
upgrade(request, server) {
|
|
762
|
+
const url = new URL(request.url);
|
|
763
|
+
if (url.pathname !== EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH) return void 0;
|
|
764
|
+
if (request.method !== "GET") {
|
|
765
|
+
return new Response("Method Not Allowed", {
|
|
766
|
+
status: 405,
|
|
767
|
+
headers: { allow: "GET" }
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
if (!this.application) return new Response("Service Unavailable", { status: 503 });
|
|
771
|
+
if (!offeredProtocol(request.headers.get("sec-websocket-protocol"))) {
|
|
772
|
+
return new Response("WebSocket protocol required", { status: 426 });
|
|
773
|
+
}
|
|
774
|
+
const connection = new EditableArtifactWebSocketConnection(this.application);
|
|
775
|
+
const upgraded = server.upgrade(request, {
|
|
776
|
+
data: connection,
|
|
777
|
+
headers: {
|
|
778
|
+
"sec-websocket-protocol": EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
return upgraded ? void 0 : new Response("Bad Request", { status: 400 });
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
var EditableArtifactWebSocketConnection = class {
|
|
785
|
+
constructor(application) {
|
|
786
|
+
this.application = application;
|
|
787
|
+
}
|
|
788
|
+
socket = null;
|
|
789
|
+
session = null;
|
|
790
|
+
work = Promise.resolve();
|
|
791
|
+
queuedMessages = 0;
|
|
792
|
+
queuedBytes = 0;
|
|
793
|
+
terminal = false;
|
|
794
|
+
attach(socket) {
|
|
795
|
+
if (this.socket || this.terminal) {
|
|
796
|
+
socket.close(4400, "invalid connection state");
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this.socket = socket;
|
|
800
|
+
}
|
|
801
|
+
receive(message) {
|
|
802
|
+
if (this.terminal) return;
|
|
803
|
+
if (typeof message === "string") {
|
|
804
|
+
void this.fail("invalid_frame");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const byteLength = message.byteLength;
|
|
808
|
+
if (byteLength === 0 || byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES || this.queuedMessages + 1 > MAX_QUEUED_MESSAGES || this.queuedBytes + byteLength > MAX_QUEUED_BYTES) {
|
|
809
|
+
void this.fail(
|
|
810
|
+
byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES ? "oversized_frame" : "slow_client"
|
|
811
|
+
);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const bytes = message instanceof Uint8Array ? Uint8Array.from(message) : new Uint8Array(message).slice();
|
|
815
|
+
this.queuedMessages += 1;
|
|
816
|
+
this.queuedBytes += bytes.byteLength;
|
|
817
|
+
const run = this.work.then(async () => {
|
|
818
|
+
try {
|
|
819
|
+
await this.process(bytes);
|
|
820
|
+
} finally {
|
|
821
|
+
this.queuedMessages -= 1;
|
|
822
|
+
this.queuedBytes -= bytes.byteLength;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
this.work = run.catch(async (error) => {
|
|
826
|
+
await this.handleFailure(error);
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
async send(frame) {
|
|
830
|
+
if (this.terminal || !this.socket) {
|
|
831
|
+
throw new EditableArtifactLiveError("closed", "Editable artifact socket is closed");
|
|
832
|
+
}
|
|
833
|
+
const bytes = encodeEditableArtifactLiveServerWireFrame(frame);
|
|
834
|
+
if (bytes.byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES) {
|
|
835
|
+
throw new EditableArtifactLiveError(
|
|
836
|
+
"oversized_frame",
|
|
837
|
+
"Editable artifact server frame exceeds socket limit",
|
|
838
|
+
{ requiresSnapshot: true }
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
const accepted = this.socket.send(bytes, false);
|
|
842
|
+
if (!Number.isSafeInteger(accepted) || accepted <= 0) {
|
|
843
|
+
throw new EditableArtifactLiveError("closed", "Editable artifact socket rejected frame");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
bufferedBytes() {
|
|
847
|
+
const buffered = this.socket?.getBufferedAmount?.() ?? this.socket?.bufferedAmount ?? 0;
|
|
848
|
+
return Number.isSafeInteger(buffered) && buffered >= 0 ? buffered : Number.MAX_SAFE_INTEGER;
|
|
849
|
+
}
|
|
850
|
+
close(close) {
|
|
851
|
+
if (this.terminal) return;
|
|
852
|
+
this.terminal = true;
|
|
853
|
+
this.socket?.close(closeCode(close.reason), close.reason);
|
|
854
|
+
}
|
|
855
|
+
transportClosed() {
|
|
856
|
+
if (this.terminal) return;
|
|
857
|
+
this.terminal = true;
|
|
858
|
+
void this.session?.close("transport_error");
|
|
859
|
+
}
|
|
860
|
+
async process(bytes) {
|
|
861
|
+
if (this.terminal) return;
|
|
862
|
+
const frame = decodeEditableArtifactLiveClientWireFrame(bytes);
|
|
863
|
+
if (!this.session) {
|
|
864
|
+
if (frame.type !== "open") {
|
|
865
|
+
throw new EditableArtifactLiveError("invalid_frame", "First socket frame must be open");
|
|
866
|
+
}
|
|
867
|
+
const session = await this.application.openLive({
|
|
868
|
+
token: frame.token,
|
|
869
|
+
artifactId: frame.artifactId,
|
|
870
|
+
protocolVersion: frame.protocolVersion,
|
|
871
|
+
resume: frame.resume,
|
|
872
|
+
sink: this
|
|
873
|
+
});
|
|
874
|
+
if (this.terminal) {
|
|
875
|
+
await session.close("transport_error");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.session = session;
|
|
879
|
+
void session.closed.then(() => {
|
|
880
|
+
if (!this.terminal)
|
|
881
|
+
this.close({
|
|
882
|
+
reason: "closed",
|
|
883
|
+
retryable: false,
|
|
884
|
+
requiresSnapshot: false
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (frame.type === "open") {
|
|
890
|
+
throw new EditableArtifactLiveError("invalid_frame", "Socket is already open");
|
|
891
|
+
}
|
|
892
|
+
if (frame.artifactId !== this.session.artifactId || frame.streamEpoch !== this.session.streamEpoch) {
|
|
893
|
+
throw new EditableArtifactLiveError("stale_epoch", "Socket frame belongs to another stream");
|
|
894
|
+
}
|
|
895
|
+
if (frame.type === "applied") {
|
|
896
|
+
await this.session.acknowledge(frame);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
let receipt;
|
|
900
|
+
try {
|
|
901
|
+
receipt = await this.session.submitIntent(frame);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
await this.send({
|
|
904
|
+
type: "mutationRejected",
|
|
905
|
+
protocolVersion: frame.protocolVersion,
|
|
906
|
+
artifactId: frame.artifactId,
|
|
907
|
+
streamEpoch: frame.streamEpoch,
|
|
908
|
+
requestHash: frame.requestHash,
|
|
909
|
+
...mutationFailure(error)
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
await this.send({
|
|
914
|
+
type: "transaction",
|
|
915
|
+
protocolVersion: frame.protocolVersion,
|
|
916
|
+
artifactId: frame.artifactId,
|
|
917
|
+
streamEpoch: frame.streamEpoch,
|
|
918
|
+
transaction: receipt.transaction
|
|
919
|
+
});
|
|
920
|
+
await this.send({
|
|
921
|
+
type: "mutationAccepted",
|
|
922
|
+
protocolVersion: frame.protocolVersion,
|
|
923
|
+
artifactId: frame.artifactId,
|
|
924
|
+
streamEpoch: frame.streamEpoch,
|
|
925
|
+
requestHash: receipt.requestHash,
|
|
926
|
+
clientTransactionId: receipt.clientTransactionId,
|
|
927
|
+
transactionId: receipt.transaction.transactionId,
|
|
928
|
+
startSequence: receipt.transaction.startSequence,
|
|
929
|
+
endSequence: receipt.transaction.endSequence,
|
|
930
|
+
stateHash: receipt.transaction.stateHash
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
async handleFailure(error) {
|
|
934
|
+
if (this.terminal) return;
|
|
935
|
+
if (error instanceof EditableArtifactLiveError) {
|
|
936
|
+
await this.fail(error.code === "invalid_ticket" ? "transport_error" : error.code);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
await this.fail("transport_error");
|
|
940
|
+
}
|
|
941
|
+
async fail(reason) {
|
|
942
|
+
if (this.terminal) return;
|
|
943
|
+
this.terminal = true;
|
|
944
|
+
await this.session?.close(reason).catch(() => void 0);
|
|
945
|
+
this.socket?.close(closeCode(reason ?? "transport_error"), reason ?? "transport_error");
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
function mutationFailure(error) {
|
|
949
|
+
if (error instanceof EditableArtifactDomainError) {
|
|
950
|
+
if (error.code === "forbidden") return { code: "forbidden", retryable: false };
|
|
951
|
+
if (["invalid_request", "request_hash_mismatch", "invalid_undo_target"].includes(error.code)) {
|
|
952
|
+
return { code: "invalid_request", retryable: false };
|
|
953
|
+
}
|
|
954
|
+
if (error.code === "kernel_contract_violation") {
|
|
955
|
+
return { code: "unsupported", retryable: false };
|
|
956
|
+
}
|
|
957
|
+
if (["retryable_conflict", "outbox_lease_conflict"].includes(error.code)) {
|
|
958
|
+
return { code: "conflict", retryable: true };
|
|
959
|
+
}
|
|
960
|
+
return { code: "conflict", retryable: false };
|
|
961
|
+
}
|
|
962
|
+
if (error instanceof EditableArtifactLiveError && error.code === "permission_changed") {
|
|
963
|
+
return { code: "forbidden", retryable: false };
|
|
964
|
+
}
|
|
965
|
+
return { code: "unavailable", retryable: true };
|
|
966
|
+
}
|
|
967
|
+
function offeredProtocol(value) {
|
|
968
|
+
return value?.split(",").map((part) => part.trim()).includes(EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL) ?? false;
|
|
969
|
+
}
|
|
970
|
+
function closeCode(reason) {
|
|
971
|
+
if (reason === "permission_changed") return 4403;
|
|
972
|
+
if (reason === "ticket_expired" || reason === "ticket_replayed") return 4401;
|
|
973
|
+
if (reason === "protocol_mismatch") return 4406;
|
|
974
|
+
if (reason === "slow_client" || reason === "oversized_frame") return 4409;
|
|
975
|
+
if (reason === "closed") return 1e3;
|
|
976
|
+
return 4400;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/editable-artifact-production.ts
|
|
980
|
+
import { createHash as createHash4, randomUUID } from "crypto";
|
|
981
|
+
import {
|
|
982
|
+
editableArtifactMaterializerCapabilitiesForRuntime
|
|
983
|
+
} from "@opengeni/artifact-tool/runtime/materializer";
|
|
984
|
+
import {
|
|
985
|
+
EditableArtifactDurableExportService,
|
|
986
|
+
EditableArtifactAgentApplication,
|
|
987
|
+
EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES,
|
|
988
|
+
EditableArtifactCompactionPipeline,
|
|
989
|
+
EditableArtifactGenesisPipeline,
|
|
990
|
+
EditableArtifactService,
|
|
991
|
+
MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES as MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES3,
|
|
992
|
+
ProductionEditableArtifactSnapshotVerifier,
|
|
993
|
+
SystemEditableArtifactClock,
|
|
994
|
+
editableArtifactLiveReadPortFromPostgres,
|
|
995
|
+
editableArtifactLiveTicketStorePortFromPostgres,
|
|
996
|
+
editableArtifactStorePortFromPostgres,
|
|
997
|
+
editableArtifactDurableExportStorePortFromPostgres,
|
|
998
|
+
ogatxEditableArtifactMutationIntentCodec,
|
|
999
|
+
editableArtifactId
|
|
1000
|
+
} from "@opengeni/core/editable-artifacts";
|
|
1001
|
+
import {
|
|
1002
|
+
EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
|
|
1003
|
+
EditableArtifactApplication,
|
|
1004
|
+
EditableArtifactCompatibilityError,
|
|
1005
|
+
EditableArtifactLiveServer,
|
|
1006
|
+
SystemEditableArtifactLiveClock,
|
|
1007
|
+
SystemEditableArtifactLiveScheduler,
|
|
1008
|
+
WebCryptoEditableArtifactLiveTokens
|
|
1009
|
+
} from "@opengeni/core/editable-artifact-live";
|
|
1010
|
+
import {
|
|
1011
|
+
PostgresEditableArtifactLiveReadStore,
|
|
1012
|
+
PostgresEditableArtifactLiveTicketStore,
|
|
1013
|
+
PostgresEditableArtifactStore,
|
|
1014
|
+
listEditableArtifactIdsForSession,
|
|
1015
|
+
touchEditableArtifactSessionLink,
|
|
1016
|
+
dbSql,
|
|
1017
|
+
withRlsContext
|
|
1018
|
+
} from "@opengeni/db";
|
|
1019
|
+
import { PostgresEditableArtifactDurableExportStore } from "@opengeni/db/editable-artifact-durable-export";
|
|
1020
|
+
import {
|
|
1021
|
+
MAX_BOUNDED_OBJECT_CHUNK_BYTES as MAX_BOUNDED_OBJECT_CHUNK_BYTES2,
|
|
1022
|
+
createObjectStorageBoundedPorts
|
|
1023
|
+
} from "@opengeni/storage";
|
|
1024
|
+
|
|
1025
|
+
// src/editable-artifact-live-hints.ts
|
|
1026
|
+
import {
|
|
1027
|
+
decodeEditableArtifactBrokerHint,
|
|
1028
|
+
editableArtifactLiveHintSubject
|
|
1029
|
+
} from "@opengeni/core";
|
|
1030
|
+
var EventBusEditableArtifactLiveHints = class {
|
|
1031
|
+
constructor(bus) {
|
|
1032
|
+
this.bus = bus;
|
|
1033
|
+
}
|
|
1034
|
+
async subscribe(input) {
|
|
1035
|
+
const connection = this.bus.getOpStreamConnection?.();
|
|
1036
|
+
if (!connection) throw new Error("Event bus has no artifact hint subscription transport");
|
|
1037
|
+
const subject = editableArtifactLiveHintSubject(input.scope, input.artifactId);
|
|
1038
|
+
const subscription = connection.subscribe(subject);
|
|
1039
|
+
let active = true;
|
|
1040
|
+
try {
|
|
1041
|
+
await requireFlushBarrier(connection);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
subscription.unsubscribe();
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
void (async () => {
|
|
1047
|
+
try {
|
|
1048
|
+
for await (const message of subscription) {
|
|
1049
|
+
if (!active) return;
|
|
1050
|
+
try {
|
|
1051
|
+
const hint = decodeEditableArtifactBrokerHint(message.data, {
|
|
1052
|
+
scope: input.scope,
|
|
1053
|
+
artifactId: input.artifactId
|
|
1054
|
+
});
|
|
1055
|
+
input.onHint({
|
|
1056
|
+
artifactId: input.artifactId,
|
|
1057
|
+
headSequence: hint.headSequence
|
|
1058
|
+
});
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
} catch {
|
|
1063
|
+
if (active) input.onReconnect();
|
|
1064
|
+
}
|
|
1065
|
+
})();
|
|
1066
|
+
return () => {
|
|
1067
|
+
if (!active) return;
|
|
1068
|
+
active = false;
|
|
1069
|
+
subscription.unsubscribe();
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
async function requireFlushBarrier(connection) {
|
|
1074
|
+
if (!connection.flush) {
|
|
1075
|
+
throw new Error("Artifact hint subscription transport lacks a flush barrier");
|
|
1076
|
+
}
|
|
1077
|
+
await connection.flush();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/editable-artifact-native-kernel.ts
|
|
1081
|
+
import { createHash } from "crypto";
|
|
1082
|
+
import { pathToFileURL } from "url";
|
|
1083
|
+
import {
|
|
1084
|
+
createNativeArtifactSession,
|
|
1085
|
+
openNativeArtifactSession
|
|
1086
|
+
} from "@opengeni/artifact-tool/native";
|
|
1087
|
+
import {
|
|
1088
|
+
doctorConfiguredArtifactRuntime
|
|
1089
|
+
} from "@opengeni/artifact-tool/runtime/development";
|
|
1090
|
+
import {
|
|
1091
|
+
decodeEditableArtifactCausalFrontier,
|
|
1092
|
+
encodeEditableArtifactCausalFrontier
|
|
1093
|
+
} from "@opengeni/contracts/editable-artifact-causal-frontier";
|
|
1094
|
+
import { decodeEditableArtifactSerializedCommit } from "@opengeni/contracts/editable-artifact-serialized-commit";
|
|
1095
|
+
import { decodeEditableArtifactMutationIntent } from "@opengeni/contracts/editable-artifacts";
|
|
1096
|
+
import {
|
|
1097
|
+
MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES,
|
|
1098
|
+
causalFrontiersEqual,
|
|
1099
|
+
editableArtifactCausalFrontier,
|
|
1100
|
+
editableArtifactContentHash,
|
|
1101
|
+
editableArtifactReplicaId,
|
|
1102
|
+
editableArtifactStateHash
|
|
1103
|
+
} from "@opengeni/core";
|
|
1104
|
+
var MODEL_SCHEMA_VERSION = 1;
|
|
1105
|
+
var OPERATION_PROTOCOL_VERSION = 1;
|
|
1106
|
+
var CRDT_STATE_VERSION = 1;
|
|
1107
|
+
var SNAPSHOT_CHUNK_BYTES = 1024 * 1024;
|
|
1108
|
+
async function loadVerifiedNativeArtifactRuntimeBinding() {
|
|
1109
|
+
const location = await doctorConfiguredArtifactRuntime();
|
|
1110
|
+
const module = await import(
|
|
1111
|
+
/* @vite-ignore */
|
|
1112
|
+
pathToFileURL(location.skillFacadeEntrypoint).href
|
|
1113
|
+
);
|
|
1114
|
+
const candidate = module.getConfiguredArtifactRuntime?.();
|
|
1115
|
+
assertRuntime(candidate, location);
|
|
1116
|
+
return Object.freeze({ runtime: candidate, location, facade: module });
|
|
1117
|
+
}
|
|
1118
|
+
var NativeEditableArtifactKernelAdapter = class {
|
|
1119
|
+
constructor(runtime, objects) {
|
|
1120
|
+
this.runtime = runtime;
|
|
1121
|
+
this.objects = objects;
|
|
1122
|
+
if (runtime.kind !== "native" || runtime.target === "wasm-web") {
|
|
1123
|
+
throw new Error("Editable artifact authority requires a native kernel");
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async applyTransaction(request) {
|
|
1127
|
+
const session = await this.openCurrentState(request.state);
|
|
1128
|
+
try {
|
|
1129
|
+
if (request.modality === "spreadsheet") {
|
|
1130
|
+
if (session.modality !== "spreadsheet") throw new Error("Native modality mismatch");
|
|
1131
|
+
const committedTransactionBytes = session.authorTransaction(
|
|
1132
|
+
request.intentBytes,
|
|
1133
|
+
encodeEditableArtifactCausalFrontier(request.resolvedCausalBase)
|
|
1134
|
+
);
|
|
1135
|
+
return Object.freeze({
|
|
1136
|
+
modality: "spreadsheet",
|
|
1137
|
+
committedTransactionBytes,
|
|
1138
|
+
kernelVersion: this.runtime.buildIdentity,
|
|
1139
|
+
modelSchemaVersion: MODEL_SCHEMA_VERSION
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
if (session.modality !== request.modality) {
|
|
1143
|
+
throw new Error("Native modality mismatch");
|
|
1144
|
+
}
|
|
1145
|
+
const nativeReceiptBytes = session.applyCommands(request.intent.commandBytes);
|
|
1146
|
+
return Object.freeze({
|
|
1147
|
+
modality: request.modality,
|
|
1148
|
+
nativeReceiptBytes,
|
|
1149
|
+
resultingStateHash: editableArtifactStateHash(session.stateHash()),
|
|
1150
|
+
kernelVersion: this.runtime.buildIdentity,
|
|
1151
|
+
modelSchemaVersion: MODEL_SCHEMA_VERSION
|
|
1152
|
+
});
|
|
1153
|
+
} finally {
|
|
1154
|
+
session.dispose();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async query(input) {
|
|
1158
|
+
const session = await this.openCurrentState(input.state);
|
|
1159
|
+
try {
|
|
1160
|
+
return Uint8Array.from(session.query(input.queryBytes));
|
|
1161
|
+
} finally {
|
|
1162
|
+
session.dispose();
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async openCurrentState(state) {
|
|
1166
|
+
const snapshot = state.snapshot;
|
|
1167
|
+
if (!snapshot) throw new Error("Editable artifact has no verified replay snapshot");
|
|
1168
|
+
const snapshotBytes = await readVerifiedSnapshot(this.objects, snapshot);
|
|
1169
|
+
const session = openNativeArtifactSession(this.runtime, {
|
|
1170
|
+
modality: state.modality,
|
|
1171
|
+
snapshot: snapshotBytes
|
|
1172
|
+
});
|
|
1173
|
+
try {
|
|
1174
|
+
verifySnapshotBoundary(session, state);
|
|
1175
|
+
replayKernelTail(session, state);
|
|
1176
|
+
verifyDurableHead(session, state);
|
|
1177
|
+
return session;
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
session.dispose();
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async createCanonicalEmptySnapshot(input) {
|
|
1184
|
+
throwIfAborted(input.signal);
|
|
1185
|
+
const session = createNativeArtifactSession(this.runtime, {
|
|
1186
|
+
modality: input.modality,
|
|
1187
|
+
replicaNamespace: namespaceForArtifact(input.artifactId)
|
|
1188
|
+
});
|
|
1189
|
+
try {
|
|
1190
|
+
const snapshot = session.snapshot();
|
|
1191
|
+
throwIfAborted(input.signal);
|
|
1192
|
+
const revision = safeRevision(session.revision());
|
|
1193
|
+
if (revision !== 0) throw new Error("Native genesis revision is not zero");
|
|
1194
|
+
const common = {
|
|
1195
|
+
scope: input.scope,
|
|
1196
|
+
artifactId: input.artifactId,
|
|
1197
|
+
modality: input.modality,
|
|
1198
|
+
canonicalChunks: fixedChunks(snapshot, input.signal),
|
|
1199
|
+
canonicalByteSize: snapshot.byteLength,
|
|
1200
|
+
canonicalContentHash: editableArtifactContentHash(
|
|
1201
|
+
`sha256:${createHash("sha256").update(snapshot).digest("hex")}`
|
|
1202
|
+
),
|
|
1203
|
+
stateHash: editableArtifactStateHash(session.stateHash()),
|
|
1204
|
+
coveredHeadSequence: 0,
|
|
1205
|
+
modelSchemaVersion: MODEL_SCHEMA_VERSION,
|
|
1206
|
+
kernelVersion: this.runtime.buildIdentity
|
|
1207
|
+
};
|
|
1208
|
+
if (session.modality === "spreadsheet") {
|
|
1209
|
+
return Object.freeze({
|
|
1210
|
+
...common,
|
|
1211
|
+
modality: "spreadsheet",
|
|
1212
|
+
coveredCausalFrontier: domainFrontier(session.frontier()),
|
|
1213
|
+
operationProtocolVersion: OPERATION_PROTOCOL_VERSION,
|
|
1214
|
+
crdtStateVersion: CRDT_STATE_VERSION
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
return Object.freeze({
|
|
1218
|
+
...common,
|
|
1219
|
+
modality: session.modality,
|
|
1220
|
+
nativeRevision: 0
|
|
1221
|
+
});
|
|
1222
|
+
} finally {
|
|
1223
|
+
session.dispose();
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async createCanonicalSnapshot(input) {
|
|
1227
|
+
throwIfAborted(input.signal);
|
|
1228
|
+
const snapshot = input.state.snapshot;
|
|
1229
|
+
if (!snapshot) throw new Error("Editable artifact has no verified replay snapshot");
|
|
1230
|
+
const snapshotBytes = await readVerifiedSnapshot(this.objects, snapshot, input.signal);
|
|
1231
|
+
const session = openNativeArtifactSession(this.runtime, {
|
|
1232
|
+
modality: input.state.modality,
|
|
1233
|
+
snapshot: snapshotBytes
|
|
1234
|
+
});
|
|
1235
|
+
try {
|
|
1236
|
+
verifySnapshotBoundary(session, input.state);
|
|
1237
|
+
replayKernelTail(session, input.state);
|
|
1238
|
+
verifyDurableHead(session, input.state);
|
|
1239
|
+
throwIfAborted(input.signal);
|
|
1240
|
+
const canonical = session.snapshot();
|
|
1241
|
+
const common = {
|
|
1242
|
+
scope: input.state.artifact.scope,
|
|
1243
|
+
artifactId: input.state.artifact.id,
|
|
1244
|
+
modality: input.state.modality,
|
|
1245
|
+
canonicalChunks: fixedChunks(canonical, input.signal),
|
|
1246
|
+
canonicalByteSize: canonical.byteLength,
|
|
1247
|
+
canonicalContentHash: editableArtifactContentHash(
|
|
1248
|
+
`sha256:${createHash("sha256").update(canonical).digest("hex")}`
|
|
1249
|
+
),
|
|
1250
|
+
stateHash: editableArtifactStateHash(session.stateHash()),
|
|
1251
|
+
coveredHeadSequence: input.state.artifact.headSequence,
|
|
1252
|
+
modelSchemaVersion: MODEL_SCHEMA_VERSION,
|
|
1253
|
+
kernelVersion: this.runtime.buildIdentity
|
|
1254
|
+
};
|
|
1255
|
+
if (session.modality === "spreadsheet") {
|
|
1256
|
+
if (input.state.modality !== "spreadsheet") throw new Error("Native modality mismatch");
|
|
1257
|
+
return Object.freeze({
|
|
1258
|
+
...common,
|
|
1259
|
+
modality: session.modality,
|
|
1260
|
+
coveredCausalFrontier: domainFrontier(session.frontier()),
|
|
1261
|
+
operationProtocolVersion: OPERATION_PROTOCOL_VERSION,
|
|
1262
|
+
crdtStateVersion: CRDT_STATE_VERSION
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
if (session.modality !== input.state.modality) throw new Error("Native modality mismatch");
|
|
1266
|
+
return Object.freeze({
|
|
1267
|
+
...common,
|
|
1268
|
+
modality: session.modality,
|
|
1269
|
+
nativeRevision: safeRevision(session.revision())
|
|
1270
|
+
});
|
|
1271
|
+
} finally {
|
|
1272
|
+
session.dispose();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
async verifyAndReconstruct(input) {
|
|
1276
|
+
throwIfAborted(input.signal);
|
|
1277
|
+
const bytes = await collectExact(
|
|
1278
|
+
input.snapshotChunks,
|
|
1279
|
+
input.expectedSnapshotByteSize,
|
|
1280
|
+
input.signal
|
|
1281
|
+
);
|
|
1282
|
+
const session = openNativeArtifactSession(this.runtime, {
|
|
1283
|
+
modality: input.modality,
|
|
1284
|
+
snapshot: bytes
|
|
1285
|
+
});
|
|
1286
|
+
try {
|
|
1287
|
+
const canonical = session.snapshot();
|
|
1288
|
+
if (!equalBytes(canonical, bytes)) throw new Error("Native snapshot is not canonical");
|
|
1289
|
+
const expected = input.expectedSnapshot;
|
|
1290
|
+
if (expected.modality !== session.modality) throw new Error("Snapshot modality mismatch");
|
|
1291
|
+
const coveredHeadSequence = expected.coveredHeadSequence;
|
|
1292
|
+
const stateHash = editableArtifactStateHash(session.stateHash());
|
|
1293
|
+
if (stateHash !== expected.stateHash) throw new Error("Snapshot state hash mismatch");
|
|
1294
|
+
const canonicalContentHash = editableArtifactContentHash(
|
|
1295
|
+
`sha256:${createHash("sha256").update(canonical).digest("hex")}`
|
|
1296
|
+
);
|
|
1297
|
+
if (session.modality === "spreadsheet") {
|
|
1298
|
+
const coveredCausalFrontier = domainFrontier(session.frontier());
|
|
1299
|
+
if (expected.modality !== "spreadsheet" || !causalFrontiersEqual(coveredCausalFrontier, expected.coveredCausalFrontier)) {
|
|
1300
|
+
throw new Error("Snapshot causal coverage mismatch");
|
|
1301
|
+
}
|
|
1302
|
+
if (input.replayPlan && input.replayPlan.modality !== "spreadsheet") {
|
|
1303
|
+
throw new Error("Replay modality mismatch");
|
|
1304
|
+
}
|
|
1305
|
+
const replayedTarget2 = input.replayPlan ? await replaySpreadsheetPlan(session, input.replayPlan, input.signal) : void 0;
|
|
1306
|
+
return Object.freeze({
|
|
1307
|
+
modality: "spreadsheet",
|
|
1308
|
+
canonicalReencodeVerified: true,
|
|
1309
|
+
scope: input.scope,
|
|
1310
|
+
artifactId: input.artifactId,
|
|
1311
|
+
canonicalByteSize: canonical.byteLength,
|
|
1312
|
+
canonicalContentHash,
|
|
1313
|
+
coveredHeadSequence,
|
|
1314
|
+
stateHash,
|
|
1315
|
+
modelSchemaVersion: MODEL_SCHEMA_VERSION,
|
|
1316
|
+
kernelVersion: this.runtime.buildIdentity,
|
|
1317
|
+
fullCrdtStateVerified: true,
|
|
1318
|
+
coveredCausalFrontier,
|
|
1319
|
+
operationProtocolVersion: OPERATION_PROTOCOL_VERSION,
|
|
1320
|
+
crdtStateVersion: CRDT_STATE_VERSION,
|
|
1321
|
+
...replayedTarget2 ? { replayedTarget: replayedTarget2 } : {}
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (expected.modality === "spreadsheet") throw new Error("Snapshot modality mismatch");
|
|
1325
|
+
const nativeRevision = safeRevision(session.revision());
|
|
1326
|
+
if (nativeRevision !== expected.nativeRevision) {
|
|
1327
|
+
throw new Error("Snapshot native revision mismatch");
|
|
1328
|
+
}
|
|
1329
|
+
if (input.replayPlan && input.replayPlan.modality === "spreadsheet") {
|
|
1330
|
+
throw new Error("Replay modality mismatch");
|
|
1331
|
+
}
|
|
1332
|
+
const replayedTarget = input.replayPlan ? await replaySerializedPlan(session, input.replayPlan, input.signal) : void 0;
|
|
1333
|
+
return Object.freeze({
|
|
1334
|
+
modality: session.modality,
|
|
1335
|
+
canonicalReencodeVerified: true,
|
|
1336
|
+
scope: input.scope,
|
|
1337
|
+
artifactId: input.artifactId,
|
|
1338
|
+
canonicalByteSize: canonical.byteLength,
|
|
1339
|
+
canonicalContentHash,
|
|
1340
|
+
coveredHeadSequence,
|
|
1341
|
+
stateHash,
|
|
1342
|
+
modelSchemaVersion: MODEL_SCHEMA_VERSION,
|
|
1343
|
+
kernelVersion: this.runtime.buildIdentity,
|
|
1344
|
+
nativeRevision,
|
|
1345
|
+
...replayedTarget ? { replayedTarget } : {}
|
|
1346
|
+
});
|
|
1347
|
+
} finally {
|
|
1348
|
+
session.dispose();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
function assertRuntime(candidate, location) {
|
|
1353
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
1354
|
+
throw new Error("Verified artifact facade returned no runtime");
|
|
1355
|
+
}
|
|
1356
|
+
const runtime = candidate;
|
|
1357
|
+
if (runtime.kind !== "native" || runtime.target === "wasm-web" || runtime.target !== location.target || runtime.buildIdentity !== location.kernel.buildIdentity || typeof runtime.createCollaborationSession !== "function" || typeof runtime.openCollaborationSession !== "function" || typeof runtime.createDocumentSession !== "function" || typeof runtime.openDocumentSession !== "function" || typeof runtime.createPresentationSession !== "function" || typeof runtime.openPresentationSession !== "function") {
|
|
1358
|
+
throw new Error("Verified artifact facade returned an incompatible runtime");
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function verifySnapshotBoundary(session, state) {
|
|
1362
|
+
const snapshot = state.snapshot;
|
|
1363
|
+
if (!snapshot || session.modality !== state.modality)
|
|
1364
|
+
throw new Error("Snapshot modality mismatch");
|
|
1365
|
+
if (session.stateHash() !== snapshot.stateHash) {
|
|
1366
|
+
throw new Error("Native snapshot boundary does not match durable metadata");
|
|
1367
|
+
}
|
|
1368
|
+
if (state.modality === "spreadsheet") {
|
|
1369
|
+
if (session.modality !== "spreadsheet" || snapshot.modality !== "spreadsheet" || !causalFrontiersEqual(domainFrontier(session.frontier()), snapshot.coveredCausalFrontier)) {
|
|
1370
|
+
throw new Error("Native snapshot frontier does not match durable metadata");
|
|
1371
|
+
}
|
|
1372
|
+
} else if (session.modality === "spreadsheet" || snapshot.modality === "spreadsheet" || safeRevision(session.revision()) !== snapshot.nativeRevision || state.baseNativeRevision !== snapshot.nativeRevision) {
|
|
1373
|
+
throw new Error("Native snapshot revision does not match durable metadata");
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function replayKernelTail(session, state) {
|
|
1377
|
+
if (state.modality === "spreadsheet") {
|
|
1378
|
+
if (session.modality !== "spreadsheet") throw new Error("Native modality mismatch");
|
|
1379
|
+
for (const transaction of state.committedTransactionTail) {
|
|
1380
|
+
const priorNativeRevision = safeRevision(session.revision());
|
|
1381
|
+
if (session.stateHash() !== transaction.priorStateHash) {
|
|
1382
|
+
throw new Error("Spreadsheet replay prior state mismatch");
|
|
1383
|
+
}
|
|
1384
|
+
session.applyCommitted(transaction.committedTransactionBytes);
|
|
1385
|
+
if (safeRevision(session.revision()) !== priorNativeRevision + 1 || session.stateHash() !== transaction.stateHash || !causalFrontiersEqual(
|
|
1386
|
+
domainFrontier(session.frontier()),
|
|
1387
|
+
transaction.resultingCausalFrontier
|
|
1388
|
+
)) {
|
|
1389
|
+
throw new Error("Spreadsheet replay result mismatch");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (session.modality !== state.modality) {
|
|
1395
|
+
throw new Error("Native modality mismatch");
|
|
1396
|
+
}
|
|
1397
|
+
for (const transaction of state.committedTransactionTail) {
|
|
1398
|
+
if (session.stateHash() !== transaction.priorStateHash || safeRevision(session.revision()) !== transaction.priorNativeRevision) {
|
|
1399
|
+
throw new Error("Serialized replay prior state mismatch");
|
|
1400
|
+
}
|
|
1401
|
+
const commit = decodeEditableArtifactSerializedCommit(
|
|
1402
|
+
transaction.committedTransactionBytes,
|
|
1403
|
+
state.modality
|
|
1404
|
+
);
|
|
1405
|
+
const intent = decodeEditableArtifactMutationIntent(commit.intentBytes);
|
|
1406
|
+
const receipt = session.applyCommands(intent.commandBytes);
|
|
1407
|
+
if (!equalBytes(receipt, commit.nativeReceiptBytes) || !equalBytes(receipt, transaction.nativeReceiptBytes) || safeRevision(session.revision()) !== transaction.nativeRevision || session.stateHash() !== transaction.stateHash) {
|
|
1408
|
+
throw new Error("Serialized replay result mismatch");
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function verifyDurableHead(session, state) {
|
|
1413
|
+
const snapshot = state.snapshot;
|
|
1414
|
+
if (!snapshot) throw new Error("Editable artifact has no replay snapshot");
|
|
1415
|
+
let durableSequence = snapshot.coveredHeadSequence;
|
|
1416
|
+
for (const transaction of state.committedTransactionTail) {
|
|
1417
|
+
if (transaction.sequenceStart !== durableSequence + 1) {
|
|
1418
|
+
throw new Error("Durable transaction tail contains a sequence gap");
|
|
1419
|
+
}
|
|
1420
|
+
durableSequence = transaction.sequenceEnd;
|
|
1421
|
+
}
|
|
1422
|
+
if (session.modality !== state.modality || durableSequence !== state.artifact.headSequence || session.stateHash() !== state.artifact.stateHash) {
|
|
1423
|
+
throw new Error("Reconstructed native state does not match the durable head");
|
|
1424
|
+
}
|
|
1425
|
+
if (state.modality === "spreadsheet" && (session.modality !== "spreadsheet" || !causalFrontiersEqual(domainFrontier(session.frontier()), state.artifact.causalFrontier))) {
|
|
1426
|
+
throw new Error("Reconstructed native frontier does not match the durable head");
|
|
1427
|
+
}
|
|
1428
|
+
if (state.modality !== "spreadsheet") {
|
|
1429
|
+
const expectedNativeRevision = state.committedTransactionTail.at(-1)?.nativeRevision ?? state.baseNativeRevision;
|
|
1430
|
+
if (safeRevision(session.revision()) !== expectedNativeRevision) {
|
|
1431
|
+
throw new Error("Reconstructed native revision does not match the serialized tail");
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
async function replaySpreadsheetPlan(session, plan, signal) {
|
|
1436
|
+
let sequence = plan.baseHeadSequence;
|
|
1437
|
+
for await (const segment of plan.segments) {
|
|
1438
|
+
throwIfAborted(signal);
|
|
1439
|
+
if (segment.sequenceStart !== sequence + 1) throw new Error("Replay sequence gap");
|
|
1440
|
+
if (segment.modality !== "spreadsheet") throw new Error("Replay segment modality mismatch");
|
|
1441
|
+
session.applyCommitted(segment.bytes);
|
|
1442
|
+
sequence = segment.sequenceEnd;
|
|
1443
|
+
}
|
|
1444
|
+
if (sequence !== plan.targetHeadSequence || session.stateHash() !== plan.targetStateHash) {
|
|
1445
|
+
throw new Error("Replay target mismatch");
|
|
1446
|
+
}
|
|
1447
|
+
const causalFrontier = domainFrontier(session.frontier());
|
|
1448
|
+
if (!causalFrontiersEqual(causalFrontier, plan.targetCausalFrontier)) {
|
|
1449
|
+
throw new Error("Replay frontier mismatch");
|
|
1450
|
+
}
|
|
1451
|
+
return Object.freeze({
|
|
1452
|
+
headSequence: sequence,
|
|
1453
|
+
causalFrontier,
|
|
1454
|
+
stateHash: editableArtifactStateHash(session.stateHash())
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
async function replaySerializedPlan(session, plan, signal) {
|
|
1458
|
+
if (session.modality !== plan.modality) throw new Error("Replay modality mismatch");
|
|
1459
|
+
let sequence = plan.baseHeadSequence;
|
|
1460
|
+
for await (const segment of plan.segments) {
|
|
1461
|
+
throwIfAborted(signal);
|
|
1462
|
+
if (segment.sequenceStart !== sequence + 1 || segment.modality !== session.modality) {
|
|
1463
|
+
throw new Error("Replay sequence or modality mismatch");
|
|
1464
|
+
}
|
|
1465
|
+
const commit = decodeEditableArtifactSerializedCommit(segment.bytes, session.modality);
|
|
1466
|
+
const intent = decodeEditableArtifactMutationIntent(commit.intentBytes);
|
|
1467
|
+
const receipt = session.applyCommands(intent.commandBytes);
|
|
1468
|
+
if (!equalBytes(receipt, commit.nativeReceiptBytes)) throw new Error("Replay receipt mismatch");
|
|
1469
|
+
sequence = segment.sequenceEnd;
|
|
1470
|
+
}
|
|
1471
|
+
if (sequence !== plan.targetHeadSequence || session.stateHash() !== plan.targetStateHash) {
|
|
1472
|
+
throw new Error("Replay target mismatch");
|
|
1473
|
+
}
|
|
1474
|
+
if (safeRevision(session.revision()) !== plan.targetNativeRevision) {
|
|
1475
|
+
throw new Error("Replay native revision mismatch");
|
|
1476
|
+
}
|
|
1477
|
+
return Object.freeze({
|
|
1478
|
+
headSequence: sequence,
|
|
1479
|
+
nativeRevision: plan.targetNativeRevision,
|
|
1480
|
+
stateHash: editableArtifactStateHash(session.stateHash())
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
function domainFrontier(bytes) {
|
|
1484
|
+
return editableArtifactCausalFrontier(
|
|
1485
|
+
decodeEditableArtifactCausalFrontier(bytes).map((entry) => ({
|
|
1486
|
+
replicaId: editableArtifactReplicaId(entry.replicaId),
|
|
1487
|
+
counter: entry.counter
|
|
1488
|
+
}))
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
async function readVerifiedSnapshot(objects, snapshot, signal) {
|
|
1492
|
+
const object = await objects.open({
|
|
1493
|
+
opaqueReference: snapshot.blobReference,
|
|
1494
|
+
maxBytes: MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES,
|
|
1495
|
+
expectedByteSize: snapshot.byteSize,
|
|
1496
|
+
...signal ? { signal } : {}
|
|
1497
|
+
});
|
|
1498
|
+
try {
|
|
1499
|
+
if (object.contentType !== void 0 && object.contentType !== snapshot.mimeType) {
|
|
1500
|
+
throw new Error("Snapshot content type mismatch");
|
|
1501
|
+
}
|
|
1502
|
+
const bytes = await collectExact(
|
|
1503
|
+
object.chunks(signal ? { signal } : {}),
|
|
1504
|
+
snapshot.byteSize,
|
|
1505
|
+
signal
|
|
1506
|
+
);
|
|
1507
|
+
const contentHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
1508
|
+
if (contentHash !== snapshot.contentHash) throw new Error("Snapshot content hash mismatch");
|
|
1509
|
+
await object.assertUnchanged(signal);
|
|
1510
|
+
return bytes;
|
|
1511
|
+
} finally {
|
|
1512
|
+
await object.close();
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
async function collectExact(chunks, expectedBytes, signal) {
|
|
1516
|
+
if (!Number.isSafeInteger(expectedBytes) || expectedBytes <= 0) {
|
|
1517
|
+
throw new Error("Snapshot byte size is invalid");
|
|
1518
|
+
}
|
|
1519
|
+
const result = new Uint8Array(expectedBytes);
|
|
1520
|
+
let offset = 0;
|
|
1521
|
+
for await (const chunk of chunks) {
|
|
1522
|
+
throwIfAborted(signal);
|
|
1523
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength === 0) {
|
|
1524
|
+
throw new Error("Snapshot stream is truncated");
|
|
1525
|
+
}
|
|
1526
|
+
if (offset + chunk.byteLength > result.byteLength) {
|
|
1527
|
+
throw new Error("Snapshot stream exceeds its declared size");
|
|
1528
|
+
}
|
|
1529
|
+
result.set(chunk, offset);
|
|
1530
|
+
offset += chunk.byteLength;
|
|
1531
|
+
}
|
|
1532
|
+
throwIfAborted(signal);
|
|
1533
|
+
if (offset !== result.byteLength) throw new Error("Snapshot stream is truncated");
|
|
1534
|
+
return result;
|
|
1535
|
+
}
|
|
1536
|
+
async function* fixedChunks(bytes, signal) {
|
|
1537
|
+
for (let offset = 0; offset < bytes.byteLength; offset += SNAPSHOT_CHUNK_BYTES) {
|
|
1538
|
+
throwIfAborted(signal);
|
|
1539
|
+
yield bytes.subarray(offset, Math.min(bytes.byteLength, offset + SNAPSHOT_CHUNK_BYTES));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function namespaceForArtifact(artifactId) {
|
|
1543
|
+
const digest = createHash("sha256").update(artifactId).digest();
|
|
1544
|
+
let value = 0n;
|
|
1545
|
+
for (let index = 0; index < 8; index += 1) value = value << 8n | BigInt(digest[index]);
|
|
1546
|
+
return value === 0n ? 1n : value;
|
|
1547
|
+
}
|
|
1548
|
+
function safeRevision(value) {
|
|
1549
|
+
if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
1550
|
+
throw new Error("Native revision exceeds the safe integer range");
|
|
1551
|
+
}
|
|
1552
|
+
return Number(value);
|
|
1553
|
+
}
|
|
1554
|
+
function equalBytes(left, right) {
|
|
1555
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
1556
|
+
let difference = 0;
|
|
1557
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
1558
|
+
difference |= left[index] ^ right[index];
|
|
1559
|
+
}
|
|
1560
|
+
return difference === 0;
|
|
1561
|
+
}
|
|
1562
|
+
function throwIfAborted(signal) {
|
|
1563
|
+
if (signal?.aborted) throw new Error("Editable artifact native operation was cancelled");
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/editable-artifact-office-import.ts
|
|
1567
|
+
import { createHash as createHash2 } from "crypto";
|
|
1568
|
+
import {
|
|
1569
|
+
ArtifactOfficeSourceUnsupportedError,
|
|
1570
|
+
prepareArtifactOfficeImport
|
|
1571
|
+
} from "@opengeni/artifact-tool/office-import";
|
|
1572
|
+
import {
|
|
1573
|
+
MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES as MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES2,
|
|
1574
|
+
EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES,
|
|
1575
|
+
editableArtifactCausalFrontier as editableArtifactCausalFrontier2,
|
|
1576
|
+
editableArtifactContentHash as editableArtifactContentHash2,
|
|
1577
|
+
editableArtifactReplicaId as editableArtifactReplicaId2,
|
|
1578
|
+
editableArtifactStateHash as editableArtifactStateHash2,
|
|
1579
|
+
EditableArtifactOfficeImportError
|
|
1580
|
+
} from "@opengeni/core/editable-artifacts";
|
|
1581
|
+
import { getFile } from "@opengeni/db";
|
|
1582
|
+
import {
|
|
1583
|
+
MAX_BOUNDED_OBJECT_CHUNK_BYTES
|
|
1584
|
+
} from "@opengeni/storage";
|
|
1585
|
+
var SNAPSHOT_MIME = "application/vnd.opengeni.editable-artifact-snapshot";
|
|
1586
|
+
var EditableArtifactOfficeImportAdapter = class {
|
|
1587
|
+
constructor(dependencies) {
|
|
1588
|
+
this.dependencies = dependencies;
|
|
1589
|
+
this.readFile = dependencies.readFile ?? getFile;
|
|
1590
|
+
}
|
|
1591
|
+
readFile;
|
|
1592
|
+
async prepare(input) {
|
|
1593
|
+
throwIfAborted2(input.signal);
|
|
1594
|
+
const file = await this.readFile(this.dependencies.db, input.scope.workspaceId, input.fileId);
|
|
1595
|
+
if (!file || file.status !== "ready") {
|
|
1596
|
+
throw new EditableArtifactOfficeImportError("invalid_source");
|
|
1597
|
+
}
|
|
1598
|
+
const expectedMimeType = officeMimeType(input.modality);
|
|
1599
|
+
if (file.contentType !== expectedMimeType || !officeFilenameMatches(file.filename, input.modality) || !Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 1 || file.sizeBytes > EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES || typeof file.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(file.sha256)) {
|
|
1600
|
+
throw new EditableArtifactOfficeImportError("invalid_source");
|
|
1601
|
+
}
|
|
1602
|
+
const sourceBytes = await readVerifiedWorkspaceFile(
|
|
1603
|
+
this.dependencies.objectStorage,
|
|
1604
|
+
file,
|
|
1605
|
+
input.signal
|
|
1606
|
+
);
|
|
1607
|
+
const prepared = await prepareOfficeImport({
|
|
1608
|
+
runtime: this.dependencies.runtime,
|
|
1609
|
+
modality: input.modality,
|
|
1610
|
+
filename: file.filename,
|
|
1611
|
+
mimeType: expectedMimeType,
|
|
1612
|
+
bytes: sourceBytes,
|
|
1613
|
+
prepare: this.dependencies.prepareOffice ?? prepareArtifactOfficeImport
|
|
1614
|
+
});
|
|
1615
|
+
const retainedSource = await this.dependencies.sourceObjects.write({
|
|
1616
|
+
chunks: byteChunks(sourceBytes),
|
|
1617
|
+
contentType: expectedMimeType,
|
|
1618
|
+
maxBytes: EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES,
|
|
1619
|
+
expectedByteSize: prepared.source.byteSize,
|
|
1620
|
+
expectedContentHash: prepared.source.contentHash,
|
|
1621
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1622
|
+
});
|
|
1623
|
+
const retainedSnapshot = await this.dependencies.snapshotObjects.write({
|
|
1624
|
+
chunks: byteChunks(prepared.snapshot.bytes),
|
|
1625
|
+
contentType: SNAPSHOT_MIME,
|
|
1626
|
+
maxBytes: MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES2,
|
|
1627
|
+
expectedByteSize: prepared.snapshot.byteSize,
|
|
1628
|
+
expectedContentHash: prepared.snapshot.contentHash,
|
|
1629
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1630
|
+
});
|
|
1631
|
+
const common = {
|
|
1632
|
+
modality: prepared.snapshot.modality,
|
|
1633
|
+
blobReference: retainedSnapshot.opaqueReference,
|
|
1634
|
+
byteSize: prepared.snapshot.byteSize,
|
|
1635
|
+
contentHash: editableArtifactContentHash2(prepared.snapshot.contentHash),
|
|
1636
|
+
mimeType: SNAPSHOT_MIME,
|
|
1637
|
+
coveredHeadSequence: 0,
|
|
1638
|
+
stateHash: editableArtifactStateHash2(prepared.snapshot.stateHash),
|
|
1639
|
+
modelSchemaVersion: prepared.snapshot.modelSchemaVersion,
|
|
1640
|
+
kernelVersion: prepared.snapshot.kernelVersion
|
|
1641
|
+
};
|
|
1642
|
+
return Object.freeze({
|
|
1643
|
+
originalImport: Object.freeze({
|
|
1644
|
+
fileId: file.id,
|
|
1645
|
+
blobReference: retainedSource.opaqueReference,
|
|
1646
|
+
byteSize: prepared.source.byteSize,
|
|
1647
|
+
contentHash: editableArtifactContentHash2(prepared.source.contentHash),
|
|
1648
|
+
mimeType: expectedMimeType
|
|
1649
|
+
}),
|
|
1650
|
+
snapshot: prepared.snapshot.modality === "spreadsheet" ? Object.freeze({
|
|
1651
|
+
...common,
|
|
1652
|
+
modality: "spreadsheet",
|
|
1653
|
+
coveredCausalFrontier: editableArtifactCausalFrontier2(
|
|
1654
|
+
prepared.snapshot.coveredCausalFrontier.map((entry) => ({
|
|
1655
|
+
replicaId: editableArtifactReplicaId2(entry.replicaId),
|
|
1656
|
+
counter: entry.counter
|
|
1657
|
+
}))
|
|
1658
|
+
),
|
|
1659
|
+
operationProtocolVersion: prepared.snapshot.operationProtocolVersion,
|
|
1660
|
+
crdtStateVersion: prepared.snapshot.crdtStateVersion
|
|
1661
|
+
}) : Object.freeze({
|
|
1662
|
+
...common,
|
|
1663
|
+
modality: prepared.snapshot.modality,
|
|
1664
|
+
nativeRevision: prepared.snapshot.nativeRevision
|
|
1665
|
+
})
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
async function prepareOfficeImport(input) {
|
|
1670
|
+
try {
|
|
1671
|
+
return await input.prepare({
|
|
1672
|
+
facade: input.runtime.facade,
|
|
1673
|
+
modality: input.modality,
|
|
1674
|
+
filename: input.filename,
|
|
1675
|
+
mimeType: input.mimeType,
|
|
1676
|
+
bytes: input.bytes,
|
|
1677
|
+
expectedRuntimeTarget: input.runtime.location.target,
|
|
1678
|
+
expectedKernelVersion: input.runtime.runtime.buildIdentity
|
|
1679
|
+
});
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
if (error instanceof ArtifactOfficeSourceUnsupportedError) {
|
|
1682
|
+
throw new EditableArtifactOfficeImportError("unsupported_content");
|
|
1683
|
+
}
|
|
1684
|
+
throw error;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function officeMimeType(modality) {
|
|
1688
|
+
if (modality === "spreadsheet") {
|
|
1689
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
1690
|
+
}
|
|
1691
|
+
if (modality === "document") {
|
|
1692
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
1693
|
+
}
|
|
1694
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
1695
|
+
}
|
|
1696
|
+
function officeFilenameMatches(filename, modality) {
|
|
1697
|
+
const extension = modality === "spreadsheet" ? ".xlsx" : modality === "document" ? ".docx" : ".pptx";
|
|
1698
|
+
return filename.toLowerCase().endsWith(extension);
|
|
1699
|
+
}
|
|
1700
|
+
async function readVerifiedWorkspaceFile(storage, file, signal) {
|
|
1701
|
+
throwIfAborted2(signal);
|
|
1702
|
+
const before = await storage.headFile(file);
|
|
1703
|
+
if (before.ContentLength !== file.sizeBytes || before.ContentType !== file.contentType || before.Metadata?.sha256 !== file.sha256 || typeof before.VersionToken !== "string" || before.VersionToken.length < 1) {
|
|
1704
|
+
throw new EditableArtifactOfficeImportError("source_changed");
|
|
1705
|
+
}
|
|
1706
|
+
const bytes = new Uint8Array(file.sizeBytes);
|
|
1707
|
+
const digest = createHash2("sha256");
|
|
1708
|
+
for (let start = 0; start < file.sizeBytes; start += MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
|
|
1709
|
+
throwIfAborted2(signal);
|
|
1710
|
+
const end = Math.min(file.sizeBytes, start + MAX_BOUNDED_OBJECT_CHUNK_BYTES) - 1;
|
|
1711
|
+
const chunk = await storage.getFileRange(file, { start, end });
|
|
1712
|
+
if (!chunk || chunk.byteLength !== end - start + 1) {
|
|
1713
|
+
throw new EditableArtifactOfficeImportError("source_changed");
|
|
1714
|
+
}
|
|
1715
|
+
bytes.set(chunk, start);
|
|
1716
|
+
digest.update(chunk);
|
|
1717
|
+
}
|
|
1718
|
+
const after = await storage.headFile(file);
|
|
1719
|
+
const sha256 = digest.digest("hex");
|
|
1720
|
+
if (after.ContentLength !== before.ContentLength || after.ContentType !== before.ContentType || after.Metadata?.sha256 !== before.Metadata?.sha256 || after.VersionToken !== before.VersionToken || sha256 !== file.sha256) {
|
|
1721
|
+
throw new EditableArtifactOfficeImportError("source_changed");
|
|
1722
|
+
}
|
|
1723
|
+
return bytes;
|
|
1724
|
+
}
|
|
1725
|
+
async function* byteChunks(bytes) {
|
|
1726
|
+
for (let offset = 0; offset < bytes.byteLength; offset += MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
|
|
1727
|
+
yield bytes.subarray(offset, offset + MAX_BOUNDED_OBJECT_CHUNK_BYTES);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function throwIfAborted2(signal) {
|
|
1731
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Artifact import aborted");
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/editable-artifact-workspace-files.ts
|
|
1735
|
+
import { createHash as createHash3 } from "crypto";
|
|
1736
|
+
import { completeFileUpload, prepareGeneratedWorkspaceFile } from "@opengeni/db";
|
|
1737
|
+
var UPLOAD_INTENT_TTL_MS = 60 * 6e4;
|
|
1738
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
1739
|
+
var EditableArtifactWorkspaceFileAdapter = class {
|
|
1740
|
+
constructor(dependencies) {
|
|
1741
|
+
this.dependencies = dependencies;
|
|
1742
|
+
this.now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
1743
|
+
this.prepareFile = dependencies.prepareFile ?? prepareGeneratedWorkspaceFile;
|
|
1744
|
+
this.completeFile = dependencies.completeFile ?? completeFileUpload;
|
|
1745
|
+
}
|
|
1746
|
+
now;
|
|
1747
|
+
prepareFile;
|
|
1748
|
+
completeFile;
|
|
1749
|
+
async ensureMaterializationFile(input) {
|
|
1750
|
+
throwIfAborted3(input.signal);
|
|
1751
|
+
const storage = this.dependencies.objectStorage;
|
|
1752
|
+
if (!storage.headObject || !storage.putObjectStreamIfAbsent) {
|
|
1753
|
+
throw new Error("Object storage lacks immutable streaming upload support");
|
|
1754
|
+
}
|
|
1755
|
+
const download = await this.dependencies.durableExports.openMaterializationDownload({
|
|
1756
|
+
scope: input.scope,
|
|
1757
|
+
actor: input.actor,
|
|
1758
|
+
artifactId: input.artifact.id,
|
|
1759
|
+
jobId: input.jobId,
|
|
1760
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1761
|
+
});
|
|
1762
|
+
try {
|
|
1763
|
+
const sha256 = plainSha256(download.contentHash);
|
|
1764
|
+
const filename = exportFilename(input.filename, download.format);
|
|
1765
|
+
const fileId = deterministicUuid(
|
|
1766
|
+
`editable-artifact-export:file:${input.scope.workspaceId}:${input.artifact.id}:${input.versionId}:${input.jobId}`
|
|
1767
|
+
);
|
|
1768
|
+
const uploadId = deterministicUuid(
|
|
1769
|
+
`editable-artifact-export:upload:${input.scope.workspaceId}:${input.artifact.id}:${input.versionId}:${input.jobId}`
|
|
1770
|
+
);
|
|
1771
|
+
const safeFilename = `artifact-${input.artifact.id}-${input.jobId}.${download.format}`;
|
|
1772
|
+
const objectKey = `workspaces/${input.scope.workspaceId}/files/${fileId}/artifact-exports/${safeFilename}`;
|
|
1773
|
+
const prepared = await this.prepareFile(this.dependencies.db, {
|
|
1774
|
+
accountId: input.scope.accountId,
|
|
1775
|
+
workspaceId: input.scope.workspaceId,
|
|
1776
|
+
fileId,
|
|
1777
|
+
uploadId,
|
|
1778
|
+
filename,
|
|
1779
|
+
safeFilename,
|
|
1780
|
+
contentType: download.mimeType,
|
|
1781
|
+
sizeBytes: download.byteSize,
|
|
1782
|
+
sha256,
|
|
1783
|
+
bucket: storage.bucket,
|
|
1784
|
+
objectKey,
|
|
1785
|
+
expiresAt: new Date(this.now().getTime() + UPLOAD_INTENT_TTL_MS)
|
|
1786
|
+
});
|
|
1787
|
+
let file = prepared.file;
|
|
1788
|
+
if (file.status !== "ready") {
|
|
1789
|
+
const existing = await storage.headObject(objectKey);
|
|
1790
|
+
if (existing) {
|
|
1791
|
+
assertStoredExport(existing, download.byteSize, download.mimeType, sha256);
|
|
1792
|
+
} else {
|
|
1793
|
+
await storage.putObjectStreamIfAbsent({
|
|
1794
|
+
key: objectKey,
|
|
1795
|
+
contentType: download.mimeType,
|
|
1796
|
+
chunks: download.chunks(input.signal ? { signal: input.signal } : {}),
|
|
1797
|
+
byteSize: download.byteSize,
|
|
1798
|
+
sha256,
|
|
1799
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1800
|
+
});
|
|
1801
|
+
const stored = await storage.headObject(objectKey);
|
|
1802
|
+
assertStoredExport(stored, download.byteSize, download.mimeType, sha256);
|
|
1803
|
+
}
|
|
1804
|
+
await download.assertUnchanged(input.signal);
|
|
1805
|
+
throwIfAborted3(input.signal);
|
|
1806
|
+
file = await this.completeFile(this.dependencies.db, input.scope.workspaceId, uploadId);
|
|
1807
|
+
} else {
|
|
1808
|
+
assertStoredExport(
|
|
1809
|
+
await storage.headObject(objectKey),
|
|
1810
|
+
download.byteSize,
|
|
1811
|
+
download.mimeType,
|
|
1812
|
+
sha256
|
|
1813
|
+
);
|
|
1814
|
+
await download.assertUnchanged(input.signal);
|
|
1815
|
+
}
|
|
1816
|
+
if (file.id !== fileId || file.status !== "ready" || file.filename !== filename || file.contentType !== download.mimeType || file.sizeBytes !== download.byteSize || file.sha256 !== sha256) {
|
|
1817
|
+
throw new Error("Generated workspace file differs from its immutable export");
|
|
1818
|
+
}
|
|
1819
|
+
return Object.freeze({
|
|
1820
|
+
fileId: file.id,
|
|
1821
|
+
filename: file.filename,
|
|
1822
|
+
contentType: file.contentType,
|
|
1823
|
+
sizeBytes: file.sizeBytes,
|
|
1824
|
+
sha256,
|
|
1825
|
+
artifactId: input.artifact.id,
|
|
1826
|
+
versionId: input.versionId,
|
|
1827
|
+
materializationJobId: input.jobId,
|
|
1828
|
+
sourceHeadSequence: input.sourceHeadSequence,
|
|
1829
|
+
sourceStateHash: input.sourceStateHash
|
|
1830
|
+
});
|
|
1831
|
+
} finally {
|
|
1832
|
+
await download.close();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
function assertStoredExport(head, byteSize, contentType, sha256) {
|
|
1837
|
+
if (!head || head.ContentLength !== byteSize || head.ContentType !== contentType || head.Metadata?.sha256 !== sha256 || typeof head.VersionToken !== "string" || head.VersionToken.length < 1) {
|
|
1838
|
+
throw new Error("Workspace export object differs from its durable materialization");
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function exportFilename(value, format) {
|
|
1842
|
+
const expectedExtension = `.${format}`;
|
|
1843
|
+
const normalized = value.normalize("NFKC").replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-").replace(/\s+/gu, " ").trim().slice(0, 200);
|
|
1844
|
+
const stem = normalized.toLowerCase().endsWith(expectedExtension) ? normalized.slice(0, -expectedExtension.length) : normalized;
|
|
1845
|
+
return `${stem.replace(/[. ]+$/u, "").slice(0, 190) || "artifact"}${expectedExtension}`;
|
|
1846
|
+
}
|
|
1847
|
+
function plainSha256(value) {
|
|
1848
|
+
const match = /^sha256:([0-9a-f]{64})$/u.exec(value);
|
|
1849
|
+
if (!match) throw new Error("Materialized artifact content hash is invalid");
|
|
1850
|
+
return match[1];
|
|
1851
|
+
}
|
|
1852
|
+
function deterministicUuid(seed) {
|
|
1853
|
+
const bytes = createHash3("sha256").update(seed, "utf8").digest().subarray(0, 16);
|
|
1854
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
1855
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
1856
|
+
const hex = bytes.toString("hex");
|
|
1857
|
+
const value = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1858
|
+
if (!UUID_PATTERN.test(value)) throw new Error("Generated workspace file identity is invalid");
|
|
1859
|
+
return value;
|
|
1860
|
+
}
|
|
1861
|
+
function throwIfAborted3(signal) {
|
|
1862
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Artifact export aborted");
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/editable-artifact-production.ts
|
|
1866
|
+
var INVALIDATION_POLL_MS = 1e3;
|
|
1867
|
+
var TICKET_CLEANUP_MS = 3e4;
|
|
1868
|
+
async function createStandaloneEditableArtifactApplication(input) {
|
|
1869
|
+
const boundedObjects = createObjectStorageBoundedPorts(input.objectStorage);
|
|
1870
|
+
const materializationObjects = createObjectStorageBoundedPorts(input.objectStorage, {
|
|
1871
|
+
keyPrefix: "editable-artifacts/materializations/v1/sha256/"
|
|
1872
|
+
});
|
|
1873
|
+
const importSourceObjects = createObjectStorageBoundedPorts(input.objectStorage, {
|
|
1874
|
+
keyPrefix: "editable-artifacts/imports/v1/sha256/"
|
|
1875
|
+
});
|
|
1876
|
+
const runtimeBinding = await loadVerifiedNativeArtifactRuntimeBinding();
|
|
1877
|
+
const runtime = runtimeBinding.runtime;
|
|
1878
|
+
const kernel = new NativeEditableArtifactKernelAdapter(runtime, boundedObjects.read);
|
|
1879
|
+
const persistedStore = new PostgresEditableArtifactStore(input.db);
|
|
1880
|
+
const store = editableArtifactStorePortFromPostgres(persistedStore);
|
|
1881
|
+
const authorization = new PostgresEditableArtifactAuthorization(input.db);
|
|
1882
|
+
const ids = new RandomEditableArtifactStableIds();
|
|
1883
|
+
const clock = new SystemEditableArtifactClock();
|
|
1884
|
+
const snapshotVerifier = new ProductionEditableArtifactSnapshotVerifier({
|
|
1885
|
+
objects: boundedObjects.read,
|
|
1886
|
+
kernel
|
|
1887
|
+
});
|
|
1888
|
+
const genesis = new EditableArtifactGenesisPipeline({
|
|
1889
|
+
kernel,
|
|
1890
|
+
objects: boundedObjects.write,
|
|
1891
|
+
now: () => clock.now()
|
|
1892
|
+
});
|
|
1893
|
+
const compaction = new EditableArtifactCompactionPipeline({
|
|
1894
|
+
kernel,
|
|
1895
|
+
objects: boundedObjects.write,
|
|
1896
|
+
now: () => clock.now()
|
|
1897
|
+
});
|
|
1898
|
+
const domain = new EditableArtifactService({
|
|
1899
|
+
authorization,
|
|
1900
|
+
kernel,
|
|
1901
|
+
store,
|
|
1902
|
+
intentCodec: ogatxEditableArtifactMutationIntentCodec,
|
|
1903
|
+
clock,
|
|
1904
|
+
ids,
|
|
1905
|
+
snapshotVerifier,
|
|
1906
|
+
genesis,
|
|
1907
|
+
compaction
|
|
1908
|
+
});
|
|
1909
|
+
const materializerCapabilities = editableArtifactMaterializerCapabilitiesForRuntime(
|
|
1910
|
+
runtime,
|
|
1911
|
+
runtimeBinding.location.artifactTool.packageVersion
|
|
1912
|
+
);
|
|
1913
|
+
const durableExports = new EditableArtifactDurableExportService({
|
|
1914
|
+
authorization,
|
|
1915
|
+
exactSnapshots: Object.freeze({
|
|
1916
|
+
ensure: async (request) => await domain.compactCurrentHead(request)
|
|
1917
|
+
}),
|
|
1918
|
+
store: editableArtifactDurableExportStorePortFromPostgres(
|
|
1919
|
+
new PostgresEditableArtifactDurableExportStore(input.db)
|
|
1920
|
+
),
|
|
1921
|
+
ids: new RandomEditableArtifactDurableExportIds(),
|
|
1922
|
+
profiles: materializationProfiles(materializerCapabilities),
|
|
1923
|
+
materializationObjects: materializationObjects.read
|
|
1924
|
+
});
|
|
1925
|
+
const officeImports = new EditableArtifactOfficeImportAdapter({
|
|
1926
|
+
db: input.db,
|
|
1927
|
+
objectStorage: input.objectStorage,
|
|
1928
|
+
runtime: runtimeBinding,
|
|
1929
|
+
sourceObjects: importSourceObjects.write,
|
|
1930
|
+
snapshotObjects: boundedObjects.write
|
|
1931
|
+
});
|
|
1932
|
+
const agent = new EditableArtifactAgentApplication({
|
|
1933
|
+
domain,
|
|
1934
|
+
exports: durableExports,
|
|
1935
|
+
associations: Object.freeze({
|
|
1936
|
+
listArtifactIds: async ({ scope, sessionId, limit }) => (await listEditableArtifactIdsForSession(input.db, scope, sessionId, limit)).map(
|
|
1937
|
+
editableArtifactId
|
|
1938
|
+
),
|
|
1939
|
+
touch: async ({ scope, sessionId, artifactId }) => await touchEditableArtifactSessionLink(input.db, scope, sessionId, artifactId)
|
|
1940
|
+
}),
|
|
1941
|
+
inspector: kernel,
|
|
1942
|
+
officeImports,
|
|
1943
|
+
workspaceFiles: new EditableArtifactWorkspaceFileAdapter({
|
|
1944
|
+
db: input.db,
|
|
1945
|
+
objectStorage: input.objectStorage,
|
|
1946
|
+
durableExports
|
|
1947
|
+
})
|
|
1948
|
+
});
|
|
1949
|
+
const ticketStore = new PostgresEditableArtifactLiveTicketStore(input.db);
|
|
1950
|
+
const liveRead = new PostgresEditableArtifactLiveReadStore(input.db, {
|
|
1951
|
+
snapshotBytes: snapshotBytesPort(boundedObjects.read)
|
|
1952
|
+
});
|
|
1953
|
+
const invalidations = new PollingEditableArtifactAuthorizationInvalidations(authorization);
|
|
1954
|
+
const live = new EditableArtifactLiveServer({
|
|
1955
|
+
authorization,
|
|
1956
|
+
domain,
|
|
1957
|
+
tickets: editableArtifactLiveTicketStorePortFromPostgres(ticketStore),
|
|
1958
|
+
tokens: new WebCryptoEditableArtifactLiveTokens(),
|
|
1959
|
+
clock: new SystemEditableArtifactLiveClock(),
|
|
1960
|
+
scheduler: new SystemEditableArtifactLiveScheduler(),
|
|
1961
|
+
read: editableArtifactLiveReadPortFromPostgres(liveRead),
|
|
1962
|
+
hints: new EventBusEditableArtifactLiveHints(input.bus),
|
|
1963
|
+
invalidations
|
|
1964
|
+
});
|
|
1965
|
+
const compatibility = Object.freeze({
|
|
1966
|
+
assertCompatible(request) {
|
|
1967
|
+
if (request.protocolVersion !== EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION || request.modelSchemaVersion !== 1 || request.kernelVersion !== runtime.buildIdentity) {
|
|
1968
|
+
throw new EditableArtifactCompatibilityError();
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
const application = new EditableArtifactApplication({
|
|
1973
|
+
domain,
|
|
1974
|
+
live,
|
|
1975
|
+
compatibility
|
|
1976
|
+
});
|
|
1977
|
+
const cleanup = setInterval(() => {
|
|
1978
|
+
void ticketStore.cleanupExpired().catch(() => void 0);
|
|
1979
|
+
}, TICKET_CLEANUP_MS);
|
|
1980
|
+
cleanup.unref?.();
|
|
1981
|
+
return Object.freeze({
|
|
1982
|
+
application,
|
|
1983
|
+
durableExports,
|
|
1984
|
+
agent,
|
|
1985
|
+
officeImports,
|
|
1986
|
+
kernelVersion: runtime.buildIdentity,
|
|
1987
|
+
close() {
|
|
1988
|
+
clearInterval(cleanup);
|
|
1989
|
+
invalidations.close();
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
var PostgresEditableArtifactAuthorization = class {
|
|
1994
|
+
constructor(db) {
|
|
1995
|
+
this.db = db;
|
|
1996
|
+
}
|
|
1997
|
+
async authorize(request) {
|
|
1998
|
+
const actor = request.actor;
|
|
1999
|
+
const rows = await withRlsContext(
|
|
2000
|
+
this.db,
|
|
2001
|
+
request.scope,
|
|
2002
|
+
async (tx) => rawRows(
|
|
2003
|
+
await tx.execute(dbSql`select *
|
|
2004
|
+
from opengeni_private.authorize_editable_artifact_actor(
|
|
2005
|
+
${request.scope.accountId}::uuid,
|
|
2006
|
+
${request.scope.workspaceId}::uuid,
|
|
2007
|
+
${request.artifactId},
|
|
2008
|
+
${actor.kind},
|
|
2009
|
+
${actor.subjectId},
|
|
2010
|
+
${actor.kind === "agent" ? actor.sessionId : null},
|
|
2011
|
+
${actor.kind === "agent" ? actor.turnId : null},
|
|
2012
|
+
${actor.kind === "agent" ? actor.attemptId : null},
|
|
2013
|
+
${actor.kind === "agent" ? actor.generation : null},
|
|
2014
|
+
${actor.kind === "service" ? actor.service : null},
|
|
2015
|
+
${request.permission},
|
|
2016
|
+
current_schema()
|
|
2017
|
+
)`)
|
|
2018
|
+
)
|
|
2019
|
+
);
|
|
2020
|
+
if (rows.length !== 1) throw new Error("Editable artifact authorization returned no decision");
|
|
2021
|
+
const revision = Number(rows[0].authorization_revision);
|
|
2022
|
+
if (!Number.isSafeInteger(revision) || revision < 1) {
|
|
2023
|
+
throw new Error("Editable artifact authorization returned an invalid revision");
|
|
2024
|
+
}
|
|
2025
|
+
return Object.freeze({ allowed: rows[0].allowed === true, revision });
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
var PollingEditableArtifactAuthorizationInvalidations = class {
|
|
2029
|
+
constructor(authorization) {
|
|
2030
|
+
this.authorization = authorization;
|
|
2031
|
+
}
|
|
2032
|
+
stops = /* @__PURE__ */ new Set();
|
|
2033
|
+
async subscribe(input) {
|
|
2034
|
+
let active = true;
|
|
2035
|
+
let polling = false;
|
|
2036
|
+
let baseline = await this.read(input);
|
|
2037
|
+
const timer = setInterval(() => {
|
|
2038
|
+
if (!active || polling) return;
|
|
2039
|
+
polling = true;
|
|
2040
|
+
void this.read(input).then((next) => {
|
|
2041
|
+
if (!active) return;
|
|
2042
|
+
if (next !== baseline) {
|
|
2043
|
+
baseline = next;
|
|
2044
|
+
input.onInvalidated();
|
|
2045
|
+
}
|
|
2046
|
+
}).catch(() => {
|
|
2047
|
+
if (active) input.onInvalidated();
|
|
2048
|
+
}).finally(() => {
|
|
2049
|
+
polling = false;
|
|
2050
|
+
});
|
|
2051
|
+
}, INVALIDATION_POLL_MS);
|
|
2052
|
+
timer.unref?.();
|
|
2053
|
+
const stop = () => {
|
|
2054
|
+
if (!active) return;
|
|
2055
|
+
active = false;
|
|
2056
|
+
clearInterval(timer);
|
|
2057
|
+
this.stops.delete(stop);
|
|
2058
|
+
};
|
|
2059
|
+
this.stops.add(stop);
|
|
2060
|
+
return stop;
|
|
2061
|
+
}
|
|
2062
|
+
close() {
|
|
2063
|
+
for (const stop of [...this.stops]) stop();
|
|
2064
|
+
}
|
|
2065
|
+
async read(input) {
|
|
2066
|
+
const decision = await this.authorization.authorize({
|
|
2067
|
+
scope: input.scope,
|
|
2068
|
+
artifactId: input.artifactId,
|
|
2069
|
+
actor: input.actor,
|
|
2070
|
+
permission: "read"
|
|
2071
|
+
});
|
|
2072
|
+
return `${decision.allowed ? 1 : 0}:${decision.revision}`;
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
var RandomEditableArtifactStableIds = class {
|
|
2076
|
+
next(_kind) {
|
|
2077
|
+
return randomUUID().replaceAll("-", "");
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
var RandomEditableArtifactDurableExportIds = class {
|
|
2081
|
+
next(_kind) {
|
|
2082
|
+
return randomUUID().replaceAll("-", "");
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
function materializationProfiles(capabilities) {
|
|
2086
|
+
if (capabilities.protocol !== "OGAMC001" || capabilities.runtimeKind !== "native" || capabilities.maxOutputBytes !== EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES || capabilities.codecVersions["opengeni.xlsx"] === void 0 || !capabilities.supportedModelSchemaVersions.includes(1) || !capabilities.supportedOperationProtocolVersions.includes(1) || !capabilities.supportedSnapshotProtocolVersions.includes(1)) {
|
|
2087
|
+
throw new Error("Verified artifact materializer profile is incompatible");
|
|
2088
|
+
}
|
|
2089
|
+
const codecVersion = capabilities.codecVersions["opengeni.xlsx"];
|
|
2090
|
+
return Object.freeze({
|
|
2091
|
+
async resolve(input) {
|
|
2092
|
+
if (input.modality !== "spreadsheet" || input.format !== "xlsx" || Object.keys(input.options).length !== 0) {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
return Object.freeze({
|
|
2096
|
+
modality: "spreadsheet",
|
|
2097
|
+
format: "xlsx",
|
|
2098
|
+
codecId: "opengeni.xlsx",
|
|
2099
|
+
codecVersion,
|
|
2100
|
+
kernelVersion: capabilities.kernelVersion,
|
|
2101
|
+
fontRegistryHash: capabilities.fontRegistryHash,
|
|
2102
|
+
policyHash: capabilities.policyHash,
|
|
2103
|
+
normalizedOptions: "{}"
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
function snapshotBytesPort(objects) {
|
|
2109
|
+
return Object.freeze({
|
|
2110
|
+
async readSnapshotBytes(snapshot) {
|
|
2111
|
+
const object = await objects.open({
|
|
2112
|
+
opaqueReference: snapshot.blobReference,
|
|
2113
|
+
maxBytes: MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES3,
|
|
2114
|
+
expectedByteSize: snapshot.byteSize
|
|
2115
|
+
});
|
|
2116
|
+
try {
|
|
2117
|
+
if (object.contentType !== void 0 && object.contentType !== snapshot.mimeType) {
|
|
2118
|
+
throw new Error("Editable artifact snapshot content type mismatch");
|
|
2119
|
+
}
|
|
2120
|
+
const bytes = new Uint8Array(snapshot.byteSize);
|
|
2121
|
+
const digest = createHash4("sha256");
|
|
2122
|
+
let offset = 0;
|
|
2123
|
+
for await (const chunk of object.chunks({
|
|
2124
|
+
chunkBytes: MAX_BOUNDED_OBJECT_CHUNK_BYTES2
|
|
2125
|
+
})) {
|
|
2126
|
+
if (offset + chunk.byteLength > bytes.byteLength) {
|
|
2127
|
+
throw new Error("Editable artifact snapshot exceeds its declared size");
|
|
2128
|
+
}
|
|
2129
|
+
bytes.set(chunk, offset);
|
|
2130
|
+
digest.update(chunk);
|
|
2131
|
+
offset += chunk.byteLength;
|
|
2132
|
+
}
|
|
2133
|
+
if (offset !== bytes.byteLength || `sha256:${digest.digest("hex")}` !== snapshot.contentHash) {
|
|
2134
|
+
throw new Error("Editable artifact snapshot digest or size mismatch");
|
|
2135
|
+
}
|
|
2136
|
+
await object.assertUnchanged();
|
|
2137
|
+
return bytes;
|
|
2138
|
+
} finally {
|
|
2139
|
+
await object.close();
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
function rawRows(result) {
|
|
2145
|
+
if (Array.isArray(result)) return result;
|
|
2146
|
+
const rows = result?.rows;
|
|
2147
|
+
if (Array.isArray(rows)) return rows;
|
|
2148
|
+
throw new Error("Unsupported database execute result shape");
|
|
2149
|
+
}
|
|
2150
|
+
|
|
461
2151
|
// src/index.ts
|
|
462
2152
|
function isWorkflowAlreadyStarted(error) {
|
|
463
2153
|
return error instanceof WorkflowExecutionAlreadyStartedError;
|
|
@@ -562,6 +2252,14 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
562
2252
|
},
|
|
563
2253
|
syncScheduledTask: async ({ task }) => {
|
|
564
2254
|
const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
|
|
2255
|
+
if (task.schedule.type === "manual") {
|
|
2256
|
+
try {
|
|
2257
|
+
await schedule.delete();
|
|
2258
|
+
} catch (error) {
|
|
2259
|
+
if (!(error instanceof ScheduleNotFoundError)) throw error;
|
|
2260
|
+
}
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
565
2263
|
const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
|
|
566
2264
|
try {
|
|
567
2265
|
await schedule.update(() => temporalScheduleUpdateOptions(options));
|
|
@@ -573,13 +2271,21 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
573
2271
|
}
|
|
574
2272
|
},
|
|
575
2273
|
deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
|
|
576
|
-
|
|
2274
|
+
try {
|
|
2275
|
+
await temporal.withDeadline(Date.now() + 5e3, async () => {
|
|
2276
|
+
await temporal.schedule.getHandle(temporalScheduleId).delete();
|
|
2277
|
+
});
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
if (error instanceof ScheduleNotFoundError) return;
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
577
2282
|
},
|
|
578
2283
|
triggerScheduledTask: async ({
|
|
579
2284
|
task,
|
|
580
2285
|
agentRunUsageIdempotencyKey,
|
|
581
2286
|
triggerWorkflowId,
|
|
582
|
-
initiator
|
|
2287
|
+
initiator,
|
|
2288
|
+
triggerType = "manual"
|
|
583
2289
|
}) => {
|
|
584
2290
|
const workflowId = triggerWorkflowId;
|
|
585
2291
|
try {
|
|
@@ -592,7 +2298,7 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
592
2298
|
accountId: task.accountId,
|
|
593
2299
|
workspaceId: task.workspaceId,
|
|
594
2300
|
taskId: task.id,
|
|
595
|
-
triggerType
|
|
2301
|
+
triggerType,
|
|
596
2302
|
agentRunUsageIdempotencyKey,
|
|
597
2303
|
initiator
|
|
598
2304
|
}
|
|
@@ -610,18 +2316,23 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
610
2316
|
if (!targetId) {
|
|
611
2317
|
throw new Error("rig verification requires changeId or versionId");
|
|
612
2318
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
2319
|
+
try {
|
|
2320
|
+
await temporal.workflow.start("rigVerificationWorkflow", {
|
|
2321
|
+
taskQueue: settings.temporalTaskQueue,
|
|
2322
|
+
workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
|
|
2323
|
+
workflowIdReusePolicy: "REJECT_DUPLICATE",
|
|
2324
|
+
args: [
|
|
2325
|
+
{
|
|
2326
|
+
workspaceId,
|
|
2327
|
+
...changeId ? { changeId } : {},
|
|
2328
|
+
...versionId ? { versionId } : {}
|
|
2329
|
+
}
|
|
2330
|
+
]
|
|
2331
|
+
});
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
if (isWorkflowAlreadyStarted(error)) return;
|
|
2334
|
+
throw error;
|
|
2335
|
+
}
|
|
625
2336
|
},
|
|
626
2337
|
check: async () => {
|
|
627
2338
|
await connection.workflowService.getSystemInfo({});
|
|
@@ -698,24 +2409,73 @@ async function startApi() {
|
|
|
698
2409
|
await dbClient.close();
|
|
699
2410
|
throw new Error("OpenGeni API startup dependencies were not initialized");
|
|
700
2411
|
}
|
|
2412
|
+
const objectStorage = createObjectStorage(settings);
|
|
2413
|
+
let editableArtifactComposition;
|
|
2414
|
+
if (objectStorage && isArtifactRuntimeConfigured()) {
|
|
2415
|
+
try {
|
|
2416
|
+
editableArtifactComposition = await createStandaloneEditableArtifactApplication({
|
|
2417
|
+
db: dbClient.db,
|
|
2418
|
+
bus,
|
|
2419
|
+
objectStorage
|
|
2420
|
+
});
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
await Promise.allSettled([bus.close(), workflowClient.close(), dbClient.close()]);
|
|
2423
|
+
throw error;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
701
2426
|
const { app, routeDeps } = createAppComposition({
|
|
702
2427
|
settings,
|
|
703
2428
|
db: dbClient.db,
|
|
704
2429
|
bus,
|
|
705
2430
|
workflowClient: workflowClient.client,
|
|
706
2431
|
documentIndexer: workflowClient.documentIndexer,
|
|
2432
|
+
objectStorage,
|
|
2433
|
+
...editableArtifactComposition ? {
|
|
2434
|
+
editableArtifacts: editableArtifactComposition.application,
|
|
2435
|
+
editableArtifactExports: editableArtifactComposition.durableExports,
|
|
2436
|
+
editableArtifactAgent: editableArtifactComposition.agent,
|
|
2437
|
+
editableArtifactOfficeImports: editableArtifactComposition.officeImports
|
|
2438
|
+
} : {},
|
|
707
2439
|
observability,
|
|
708
2440
|
readinessChecks: {
|
|
709
2441
|
db: runtimeDatabaseReadyCheck(dbClient.db, databasePosture)
|
|
710
2442
|
}
|
|
711
2443
|
});
|
|
2444
|
+
if (!routeDeps.editableArtifacts) {
|
|
2445
|
+
observability.warn("editable artifact engine is not composed", {
|
|
2446
|
+
subsystem: "editable_artifacts",
|
|
2447
|
+
behavior: "http_and_websocket_fail_closed"
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
const artifactWebSockets = new EditableArtifactWebSocketTransport(routeDeps.editableArtifacts);
|
|
712
2451
|
const server = Bun.serve({
|
|
713
2452
|
hostname: settings.apiHost,
|
|
714
2453
|
port: settings.apiPort,
|
|
715
2454
|
idleTimeout: 255,
|
|
716
|
-
fetch:
|
|
2455
|
+
fetch: (request, bunServer) => {
|
|
2456
|
+
if (artifactWebSockets.handles(request)) {
|
|
2457
|
+
return artifactWebSockets.upgrade(request, bunServer);
|
|
2458
|
+
}
|
|
2459
|
+
return app.fetch(request);
|
|
2460
|
+
},
|
|
2461
|
+
websocket: {
|
|
2462
|
+
maxPayloadLength: EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
|
|
2463
|
+
backpressureLimit: 16 * 1024 * 1024,
|
|
2464
|
+
closeOnBackpressureLimit: true,
|
|
2465
|
+
open: (socket) => artifactWebSockets.websocket.open(socket),
|
|
2466
|
+
message: (socket, message) => artifactWebSockets.websocket.message(socket, message),
|
|
2467
|
+
close: (socket) => artifactWebSockets.websocket.close(socket)
|
|
2468
|
+
}
|
|
717
2469
|
});
|
|
718
2470
|
const stopSlackInteractionPump = settings.slackSigningSecret ? startSlackInteractionPump(routeDeps) : void 0;
|
|
2471
|
+
const stopMemorySlackPublicationPump = startMemorySlackPublicationPump(routeDeps);
|
|
2472
|
+
const stopTemporalScheduleCleanupPump = startTemporalScheduleCleanupPump({
|
|
2473
|
+
db: dbClient.db,
|
|
2474
|
+
deleteSchedule: async (temporalScheduleId) => {
|
|
2475
|
+
await workflowClient.client.deleteScheduledTaskSchedule({ temporalScheduleId });
|
|
2476
|
+
},
|
|
2477
|
+
observability
|
|
2478
|
+
});
|
|
719
2479
|
let stopMetricsIngestion;
|
|
720
2480
|
let stopHelloIngestion;
|
|
721
2481
|
let authCalloutResponder;
|
|
@@ -761,9 +2521,12 @@ async function startApi() {
|
|
|
761
2521
|
close: async () => {
|
|
762
2522
|
server.stop(true);
|
|
763
2523
|
stopSlackInteractionPump?.();
|
|
2524
|
+
await stopMemorySlackPublicationPump();
|
|
764
2525
|
stopMetricsIngestion?.();
|
|
765
2526
|
stopHelloIngestion?.();
|
|
2527
|
+
await stopTemporalScheduleCleanupPump();
|
|
766
2528
|
await Promise.allSettled([
|
|
2529
|
+
Promise.resolve(editableArtifactComposition?.close()),
|
|
767
2530
|
authCalloutResponder?.close(),
|
|
768
2531
|
bus.close(),
|
|
769
2532
|
workflowClient.close(),
|
|
@@ -788,6 +2551,9 @@ function shouldCreateScheduleAfterUpdateError(error) {
|
|
|
788
2551
|
return error instanceof ScheduleNotFoundError;
|
|
789
2552
|
}
|
|
790
2553
|
function temporalScheduleSpec(schedule) {
|
|
2554
|
+
if (schedule.type === "manual") {
|
|
2555
|
+
throw new Error("manual scheduled tasks do not have a Temporal Schedule spec");
|
|
2556
|
+
}
|
|
791
2557
|
if (schedule.type === "interval") {
|
|
792
2558
|
return {
|
|
793
2559
|
intervals: [temporalIntervalSpec(schedule)],
|
|
@@ -862,7 +2628,7 @@ function temporalScheduleOptions(task, taskQueue) {
|
|
|
862
2628
|
pauseOnFailure: false
|
|
863
2629
|
},
|
|
864
2630
|
state: {
|
|
865
|
-
paused: task
|
|
2631
|
+
paused: scheduledTaskEffectivelyPaused(task),
|
|
866
2632
|
...task.schedule.type === "once" ? { remainingActions: 1 } : {}
|
|
867
2633
|
},
|
|
868
2634
|
memo: {
|
|
@@ -873,6 +2639,14 @@ function temporalScheduleOptions(task, taskQueue) {
|
|
|
873
2639
|
}
|
|
874
2640
|
};
|
|
875
2641
|
}
|
|
2642
|
+
function scheduledTaskEffectivelyPaused(task) {
|
|
2643
|
+
if (task.status === "paused") return true;
|
|
2644
|
+
if (task.action.kind !== "knowledge_source_sync") return false;
|
|
2645
|
+
const control = task.metadata.knowledgeSourceSync;
|
|
2646
|
+
if (!control || typeof control !== "object") return false;
|
|
2647
|
+
const record = control;
|
|
2648
|
+
return record.sourceEnabled === false || record.connectionPaused === true;
|
|
2649
|
+
}
|
|
876
2650
|
function temporalScheduleUpdateOptions(options) {
|
|
877
2651
|
return {
|
|
878
2652
|
spec: options.spec,
|