@opengeni/api-router 0.22.2 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
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-JIKNR5YL.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,
|
|
@@ -41,11 +46,11 @@ function eventAttributes(attributes) {
|
|
|
41
46
|
if (!attributes) {
|
|
42
47
|
return void 0;
|
|
43
48
|
}
|
|
44
|
-
const
|
|
49
|
+
const projected = {};
|
|
45
50
|
for (const [key, value] of Object.entries(attributes)) {
|
|
46
|
-
|
|
51
|
+
projected[key] = eventAttributeValue(value);
|
|
47
52
|
}
|
|
48
|
-
return
|
|
53
|
+
return projected;
|
|
49
54
|
}
|
|
50
55
|
function eventAttributeValue(value) {
|
|
51
56
|
if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
@@ -72,6 +77,7 @@ import {
|
|
|
72
77
|
workspaceAgentPermissions
|
|
73
78
|
} from "@opengeni/events";
|
|
74
79
|
var AUTH_CALLOUT_SUBJECT = "$SYS.REQ.USER.AUTH";
|
|
80
|
+
var NATS_USER_JWT_TTL_SECONDS = 5 * 60;
|
|
75
81
|
async function handleAuthorizationRequest(deps, requestBytes) {
|
|
76
82
|
const requestJwt = Buffer.from(requestBytes).toString("utf8");
|
|
77
83
|
const decoded = decodeAuthRequest(requestJwt);
|
|
@@ -109,10 +115,14 @@ async function handleAuthorizationRequest(deps, requestBytes) {
|
|
|
109
115
|
});
|
|
110
116
|
return deny("enrollment is not active");
|
|
111
117
|
}
|
|
112
|
-
if (enrollment.id !== claims.enrollmentId) {
|
|
118
|
+
if (enrollment.workspaceId !== claims.workspaceId || enrollment.id !== claims.enrollmentId || enrollment.id !== claims.agentId || claims.agentId !== claims.enrollmentId || claims.subjectPrefix !== `agent.${claims.workspaceId}.${claims.agentId}`) {
|
|
113
119
|
return deny("enrollment identity mismatch");
|
|
114
120
|
}
|
|
121
|
+
if (enrollment.credentialGeneration !== claims.credentialGeneration) {
|
|
122
|
+
return deny("enrollment credential generation mismatch");
|
|
123
|
+
}
|
|
115
124
|
const permissions = workspaceAgentPermissions(claims.workspaceId);
|
|
125
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
116
126
|
const userJwt = mintUserJwt({
|
|
117
127
|
userPublicKey: decoded.userNkey,
|
|
118
128
|
accountSeed: deps.callout.accountSeed,
|
|
@@ -125,7 +135,7 @@ async function handleAuthorizationRequest(deps, requestBytes) {
|
|
|
125
135
|
audienceAccount: deps.callout.accountName,
|
|
126
136
|
// Tie the credential's life to the bearer's remaining life: a revoked/expired
|
|
127
137
|
// enrollment cannot outlive its bearer at the NATS layer either.
|
|
128
|
-
expiresAtSeconds: claims.exp
|
|
138
|
+
expiresAtSeconds: Math.min(claims.exp, nowSeconds + NATS_USER_JWT_TTL_SECONDS)
|
|
129
139
|
});
|
|
130
140
|
const response = mintAuthResponse({
|
|
131
141
|
userPublicKey: decoded.userNkey,
|
|
@@ -159,19 +169,26 @@ async function startAuthCalloutResponder(deps, natsUrl) {
|
|
|
159
169
|
// src/sandbox/metrics-ingestion.ts
|
|
160
170
|
import {
|
|
161
171
|
clearEnrollmentWentOffline,
|
|
172
|
+
disconnectAttachedBrowserDevices,
|
|
162
173
|
getEnrollment as getEnrollment2,
|
|
163
174
|
ingestMachineMetricsSample,
|
|
175
|
+
reconcileAttachedBrowserInventory,
|
|
164
176
|
sessionsWithActiveOpOnEnrollment,
|
|
165
177
|
setEnrollmentDisplayState,
|
|
166
178
|
setEnrollmentOpStreamState,
|
|
167
179
|
setEnrollmentWentOffline,
|
|
168
180
|
touchEnrollmentLastSeen
|
|
169
181
|
} from "@opengeni/db";
|
|
182
|
+
import {
|
|
183
|
+
AttachedBrowserInventorySnapshot as AttachedBrowserInventoryContract
|
|
184
|
+
} from "@opengeni/contracts";
|
|
170
185
|
import { appendAndPublishEvents } from "@opengeni/events";
|
|
171
186
|
import {
|
|
172
187
|
AgentEvent,
|
|
188
|
+
Arch,
|
|
173
189
|
GoingOfflineReason,
|
|
174
190
|
Hello,
|
|
191
|
+
Os,
|
|
175
192
|
goingOfflineReasonToJSON
|
|
176
193
|
} from "@opengeni/agent-proto";
|
|
177
194
|
var AGENT_EVENTS_SUBJECT = "agent.*.*.events";
|
|
@@ -210,6 +227,73 @@ function wireSampleToDbSample(wire) {
|
|
|
210
227
|
sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : /* @__PURE__ */ new Date()
|
|
211
228
|
};
|
|
212
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
|
+
}
|
|
213
297
|
async function ingestHeartbeat(db, input) {
|
|
214
298
|
const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
|
|
215
299
|
if (!enrollment) {
|
|
@@ -280,6 +364,18 @@ async function handleAgentEventPayload(db, observability, payload, subject, bus)
|
|
|
280
364
|
enrollmentId: ids.agentId,
|
|
281
365
|
reason
|
|
282
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
|
+
}
|
|
283
379
|
if (bus) {
|
|
284
380
|
const isSelfUpdate = event.event.goingOffline.reason === GoingOfflineReason.GOING_OFFLINE_REASON_UPDATE;
|
|
285
381
|
await fanOutMachineLinkEvents(
|
|
@@ -315,10 +411,23 @@ async function handleAgentEventPayload(db, observability, payload, subject, bus)
|
|
|
315
411
|
if (event.event?.$case !== "heartbeat") {
|
|
316
412
|
return;
|
|
317
413
|
}
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
|
|
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
|
+
}
|
|
321
428
|
}
|
|
429
|
+
const metrics = heartbeat.metrics;
|
|
430
|
+
if (!metrics) return;
|
|
322
431
|
try {
|
|
323
432
|
await ingestHeartbeat(db, {
|
|
324
433
|
workspaceId: ids.workspaceId,
|
|
@@ -453,6 +562,1592 @@ function startHelloIngestion(deps) {
|
|
|
453
562
|
);
|
|
454
563
|
}
|
|
455
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
|
+
|
|
456
2151
|
// src/index.ts
|
|
457
2152
|
function isWorkflowAlreadyStarted(error) {
|
|
458
2153
|
return error instanceof WorkflowExecutionAlreadyStartedError;
|
|
@@ -557,6 +2252,14 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
557
2252
|
},
|
|
558
2253
|
syncScheduledTask: async ({ task }) => {
|
|
559
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
|
+
}
|
|
560
2263
|
const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
|
|
561
2264
|
try {
|
|
562
2265
|
await schedule.update(() => temporalScheduleUpdateOptions(options));
|
|
@@ -568,13 +2271,21 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
568
2271
|
}
|
|
569
2272
|
},
|
|
570
2273
|
deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
|
|
571
|
-
|
|
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
|
+
}
|
|
572
2282
|
},
|
|
573
2283
|
triggerScheduledTask: async ({
|
|
574
2284
|
task,
|
|
575
2285
|
agentRunUsageIdempotencyKey,
|
|
576
2286
|
triggerWorkflowId,
|
|
577
|
-
initiator
|
|
2287
|
+
initiator,
|
|
2288
|
+
triggerType = "manual"
|
|
578
2289
|
}) => {
|
|
579
2290
|
const workflowId = triggerWorkflowId;
|
|
580
2291
|
try {
|
|
@@ -587,7 +2298,7 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
587
2298
|
accountId: task.accountId,
|
|
588
2299
|
workspaceId: task.workspaceId,
|
|
589
2300
|
taskId: task.id,
|
|
590
|
-
triggerType
|
|
2301
|
+
triggerType,
|
|
591
2302
|
agentRunUsageIdempotencyKey,
|
|
592
2303
|
initiator
|
|
593
2304
|
}
|
|
@@ -605,18 +2316,23 @@ async function createTemporalWorkflowClient(settings, db) {
|
|
|
605
2316
|
if (!targetId) {
|
|
606
2317
|
throw new Error("rig verification requires changeId or versionId");
|
|
607
2318
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
}
|
|
620
2336
|
},
|
|
621
2337
|
check: async () => {
|
|
622
2338
|
await connection.workflowService.getSystemInfo({});
|
|
@@ -693,24 +2409,73 @@ async function startApi() {
|
|
|
693
2409
|
await dbClient.close();
|
|
694
2410
|
throw new Error("OpenGeni API startup dependencies were not initialized");
|
|
695
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
|
+
}
|
|
696
2426
|
const { app, routeDeps } = createAppComposition({
|
|
697
2427
|
settings,
|
|
698
2428
|
db: dbClient.db,
|
|
699
2429
|
bus,
|
|
700
2430
|
workflowClient: workflowClient.client,
|
|
701
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
|
+
} : {},
|
|
702
2439
|
observability,
|
|
703
2440
|
readinessChecks: {
|
|
704
2441
|
db: runtimeDatabaseReadyCheck(dbClient.db, databasePosture)
|
|
705
2442
|
}
|
|
706
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);
|
|
707
2451
|
const server = Bun.serve({
|
|
708
2452
|
hostname: settings.apiHost,
|
|
709
2453
|
port: settings.apiPort,
|
|
710
2454
|
idleTimeout: 255,
|
|
711
|
-
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
|
+
}
|
|
712
2469
|
});
|
|
713
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
|
+
});
|
|
714
2479
|
let stopMetricsIngestion;
|
|
715
2480
|
let stopHelloIngestion;
|
|
716
2481
|
let authCalloutResponder;
|
|
@@ -733,9 +2498,11 @@ async function startApi() {
|
|
|
733
2498
|
{ db: dbClient.db, settings, callout, observability },
|
|
734
2499
|
settings.natsUrl
|
|
735
2500
|
);
|
|
736
|
-
} catch
|
|
2501
|
+
} catch {
|
|
737
2502
|
observability.error("OpenGeni NATS auth-callout responder failed to start", {
|
|
738
|
-
|
|
2503
|
+
errorClass: "NatsAuthCalloutOperationError",
|
|
2504
|
+
errorCode: "nats_auth_callout_start_failed",
|
|
2505
|
+
origin: "api"
|
|
739
2506
|
});
|
|
740
2507
|
}
|
|
741
2508
|
} else {
|
|
@@ -754,9 +2521,12 @@ async function startApi() {
|
|
|
754
2521
|
close: async () => {
|
|
755
2522
|
server.stop(true);
|
|
756
2523
|
stopSlackInteractionPump?.();
|
|
2524
|
+
await stopMemorySlackPublicationPump();
|
|
757
2525
|
stopMetricsIngestion?.();
|
|
758
2526
|
stopHelloIngestion?.();
|
|
2527
|
+
await stopTemporalScheduleCleanupPump();
|
|
759
2528
|
await Promise.allSettled([
|
|
2529
|
+
Promise.resolve(editableArtifactComposition?.close()),
|
|
760
2530
|
authCalloutResponder?.close(),
|
|
761
2531
|
bus.close(),
|
|
762
2532
|
workflowClient.close(),
|
|
@@ -781,9 +2551,12 @@ function shouldCreateScheduleAfterUpdateError(error) {
|
|
|
781
2551
|
return error instanceof ScheduleNotFoundError;
|
|
782
2552
|
}
|
|
783
2553
|
function temporalScheduleSpec(schedule) {
|
|
2554
|
+
if (schedule.type === "manual") {
|
|
2555
|
+
throw new Error("manual scheduled tasks do not have a Temporal Schedule spec");
|
|
2556
|
+
}
|
|
784
2557
|
if (schedule.type === "interval") {
|
|
785
2558
|
return {
|
|
786
|
-
intervals: [
|
|
2559
|
+
intervals: [temporalIntervalSpec(schedule)],
|
|
787
2560
|
...schedule.startAt ? { startAt: new Date(schedule.startAt) } : {},
|
|
788
2561
|
...schedule.endAt ? { endAt: new Date(schedule.endAt) } : {}
|
|
789
2562
|
};
|
|
@@ -816,6 +2589,19 @@ function temporalScheduleSpec(schedule) {
|
|
|
816
2589
|
timezone: "UTC"
|
|
817
2590
|
};
|
|
818
2591
|
}
|
|
2592
|
+
function temporalIntervalSpec(schedule) {
|
|
2593
|
+
const every = `${schedule.everySeconds}s`;
|
|
2594
|
+
if (!schedule.startAt) {
|
|
2595
|
+
return { every };
|
|
2596
|
+
}
|
|
2597
|
+
const everyMilliseconds = BigInt(schedule.everySeconds) * 1000n;
|
|
2598
|
+
const startMilliseconds = BigInt(new Date(schedule.startAt).getTime());
|
|
2599
|
+
const offsetMilliseconds = (startMilliseconds % everyMilliseconds + everyMilliseconds) % everyMilliseconds;
|
|
2600
|
+
return {
|
|
2601
|
+
every,
|
|
2602
|
+
...offsetMilliseconds === 0n ? {} : { offset: `${offsetMilliseconds}ms` }
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
819
2605
|
function temporalMonth(monthIndex) {
|
|
820
2606
|
return TEMPORAL_MONTHS[monthIndex];
|
|
821
2607
|
}
|
|
@@ -842,7 +2628,7 @@ function temporalScheduleOptions(task, taskQueue) {
|
|
|
842
2628
|
pauseOnFailure: false
|
|
843
2629
|
},
|
|
844
2630
|
state: {
|
|
845
|
-
paused: task
|
|
2631
|
+
paused: scheduledTaskEffectivelyPaused(task),
|
|
846
2632
|
...task.schedule.type === "once" ? { remainingActions: 1 } : {}
|
|
847
2633
|
},
|
|
848
2634
|
memo: {
|
|
@@ -853,6 +2639,14 @@ function temporalScheduleOptions(task, taskQueue) {
|
|
|
853
2639
|
}
|
|
854
2640
|
};
|
|
855
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
|
+
}
|
|
856
2650
|
function temporalScheduleUpdateOptions(options) {
|
|
857
2651
|
return {
|
|
858
2652
|
spec: options.spec,
|