@opengeni/db 3.7.4 → 4.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/automations.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +2 -0
- package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
- package/dist/chunk-2XEZK4LF.js.map +1 -0
- package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
- package/dist/chunk-3RI33YQN.js.map +1 -0
- package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
- package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
- package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
- package/dist/chunk-N5VXJ4I2.js.map +1 -0
- package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
- package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
- package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
- package/dist/chunk-QM6YOWHZ.js.map +1 -0
- package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
- package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
- package/dist/codemode-operations.d.ts +5 -0
- package/dist/codex-token-resolver.d.ts +6 -2
- package/dist/connection-token-resolver.d.ts +8 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/generated-images.d.ts +15 -0
- package/dist/index.d.ts +393 -91
- package/dist/index.js +9992 -6069
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +9 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-user-setup.d.ts +3 -0
- package/dist/mcp-oauth.d.ts +80 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +3 -1
- package/dist/model-catalog.d.ts +153 -0
- package/dist/model-context-snapshots.d.ts +20 -0
- package/dist/new-session-drafts.d.ts +12 -0
- package/dist/organization-membership-lifecycle.d.ts +8 -1
- package/dist/organization-model-providers.d.ts +101 -0
- package/dist/pack-components.d.ts +1 -0
- package/dist/pr-review.d.ts +9 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +8 -8
- package/dist/schema.d.ts +7708 -5050
- package/dist/schema.js +25 -1
- package/dist/session-background-commands.d.ts +18 -6
- package/dist/session-background-commands.js +15 -9
- package/dist/session-control.d.ts +37 -2
- package/dist/session-goal-pacing.d.ts +3 -6
- package/dist/session-queue-commands.d.ts +17 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/tool-gateway-approvals.d.ts +31 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-artifacts.d.ts +17 -3
- package/dist/xai-subscription.d.ts +8 -0
- package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
- package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
- package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
- package/drizzle/0390_organization_model_provider_connections.sql +197 -0
- package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
- package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
- package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
- package/drizzle/0394_session_selected_skill_activation.sql +20 -0
- package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
- package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
- package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
- package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
- package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
- package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
- package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
- package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
- package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
- package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
- package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
- package/drizzle/0406_workspace_html_site_sources.sql +71 -0
- package/package.json +6 -6
- package/src/automations.ts +9 -1
- package/src/capability-integrations.ts +8 -0
- package/src/codemode-operations.ts +31 -21
- package/src/codex-token-resolver.ts +16 -6
- package/src/connection-token-resolver.ts +52 -3
- package/src/generated-images.ts +70 -6
- package/src/index.ts +3894 -994
- package/src/insights-model-bundle.ts +54 -5
- package/src/insights.ts +53 -7
- package/src/managed-user-setup.ts +14 -3
- package/src/mcp-oauth.ts +488 -0
- package/src/migrate.ts +9 -1
- package/src/model-catalog.ts +682 -0
- package/src/model-context-snapshots.ts +151 -0
- package/src/new-session-drafts.ts +66 -21
- package/src/organization-membership-lifecycle.ts +47 -5
- package/src/organization-model-providers.ts +614 -0
- package/src/pack-components.ts +15 -2
- package/src/pr-review.ts +19 -0
- package/src/provision-roles.ts +4 -0
- package/src/runtime-posture.ts +64 -0
- package/src/schema.ts +666 -61
- package/src/session-background-commands.ts +180 -78
- package/src/session-control.ts +181 -12
- package/src/session-goal-pacing.ts +4 -8
- package/src/session-queue-commands.ts +157 -82
- package/src/tool-gateway-approvals.ts +168 -0
- package/src/workspace-artifacts.ts +633 -178
- package/src/xai-subscription.ts +66 -14
- package/dist/chunk-4NAR7FBA.js.map +0 -1
- package/dist/chunk-6DPJA36C.js.map +0 -1
- package/dist/chunk-O56DM5GZ.js.map +0 -1
- package/dist/chunk-S3TDDCS2.js.map +0 -1
- /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
- /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
- /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
- /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
- /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
- /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
WorkspaceArtifactRequestedTools,
|
|
3
|
+
type ToolGatewayIdentity,
|
|
4
|
+
type WorkspaceArtifact,
|
|
5
|
+
type WorkspaceArtifactDetailResponse,
|
|
6
|
+
type WorkspaceArtifactEvent,
|
|
7
|
+
type WorkspaceArtifactMutationResponse,
|
|
8
|
+
type WorkspaceArtifactVersion,
|
|
7
9
|
} from "@opengeni/contracts";
|
|
10
|
+
import { parseVerifiedAttemptToolCatalog } from "@opengeni/codemode";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
8
12
|
import { and, desc, eq, lt, or, sql } from "drizzle-orm";
|
|
9
13
|
import type { Database } from "./database";
|
|
10
14
|
import { withRlsContext, withWorkspaceRls } from "./database";
|
|
@@ -13,7 +17,12 @@ import * as schema from "./schema";
|
|
|
13
17
|
type ArtifactRow = typeof schema.workspaceArtifacts.$inferSelect;
|
|
14
18
|
type VersionRow = typeof schema.workspaceArtifactVersions.$inferSelect;
|
|
15
19
|
type EventRow = typeof schema.workspaceArtifactEvents.$inferSelect;
|
|
16
|
-
type ArtifactMutationToolName =
|
|
20
|
+
type ArtifactMutationToolName =
|
|
21
|
+
| "artifacts_create"
|
|
22
|
+
| "artifacts_publish"
|
|
23
|
+
| "artifacts_rollback"
|
|
24
|
+
| "artifacts_archive"
|
|
25
|
+
| "artifacts_restore";
|
|
17
26
|
|
|
18
27
|
export class WorkspaceArtifactNotFoundError extends Error {
|
|
19
28
|
readonly name = "WorkspaceArtifactNotFoundError";
|
|
@@ -47,6 +56,9 @@ function versionFromRow(row: VersionRow): WorkspaceArtifactVersion {
|
|
|
47
56
|
contentType: "text/html",
|
|
48
57
|
contentSha256: row.contentSha256,
|
|
49
58
|
sizeBytes: row.sizeBytes,
|
|
59
|
+
sourceSha256: row.sourceSha256,
|
|
60
|
+
sourceSizeBytes: row.sourceSizeBytes,
|
|
61
|
+
requestedTools: WorkspaceArtifactRequestedTools.parse(row.requestedTools),
|
|
50
62
|
sourceSessionId: row.sourceSessionId,
|
|
51
63
|
sourceTurnId: row.sourceTurnId,
|
|
52
64
|
sourceAttemptId: row.sourceAttemptId,
|
|
@@ -125,23 +137,34 @@ async function artifactRow(scopedDb: any, workspaceId: string, artifactId: strin
|
|
|
125
137
|
export async function listWorkspaceArtifacts(
|
|
126
138
|
db: Database,
|
|
127
139
|
workspaceId: string,
|
|
128
|
-
options: {
|
|
129
|
-
|
|
140
|
+
options: {
|
|
141
|
+
limit?: number;
|
|
142
|
+
cursor?: string;
|
|
143
|
+
status?: "active" | "archived";
|
|
144
|
+
} = {},
|
|
145
|
+
): Promise<{
|
|
146
|
+
artifacts: WorkspaceArtifact[];
|
|
147
|
+
nextCursor: string | null;
|
|
148
|
+
truncated: boolean;
|
|
149
|
+
}> {
|
|
130
150
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
131
151
|
const limit = Math.min(Math.max(options.limit ?? 50, 1), 100);
|
|
132
152
|
const cursor = options.cursor ? decodeListCursor(options.cursor) : null;
|
|
133
|
-
const visibility =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
const visibility = and(
|
|
154
|
+
eq(schema.workspaceArtifacts.workspaceId, workspaceId),
|
|
155
|
+
...(options.status ? [eq(schema.workspaceArtifacts.status, options.status)] : []),
|
|
156
|
+
...(cursor
|
|
157
|
+
? [
|
|
158
|
+
or(
|
|
159
|
+
lt(schema.workspaceArtifacts.updatedAt, cursor.updatedAt),
|
|
160
|
+
and(
|
|
161
|
+
eq(schema.workspaceArtifacts.updatedAt, cursor.updatedAt),
|
|
162
|
+
lt(schema.workspaceArtifacts.id, cursor.id),
|
|
163
|
+
),
|
|
141
164
|
),
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
165
|
+
]
|
|
166
|
+
: []),
|
|
167
|
+
);
|
|
145
168
|
const rows = await scopedDb
|
|
146
169
|
.select()
|
|
147
170
|
.from(schema.workspaceArtifacts)
|
|
@@ -237,7 +260,12 @@ export async function getWorkspaceArtifactContentRef(
|
|
|
237
260
|
workspaceId: string,
|
|
238
261
|
artifactId: string,
|
|
239
262
|
versionId?: string,
|
|
240
|
-
): Promise<{
|
|
263
|
+
): Promise<{
|
|
264
|
+
artifactId: string;
|
|
265
|
+
version: WorkspaceArtifactVersion;
|
|
266
|
+
contentKey: string;
|
|
267
|
+
sourceKey: string | null;
|
|
268
|
+
}> {
|
|
241
269
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
242
270
|
const artifact = await artifactRow(scopedDb, workspaceId, artifactId);
|
|
243
271
|
if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
|
|
@@ -255,7 +283,12 @@ export async function getWorkspaceArtifactContentRef(
|
|
|
255
283
|
)
|
|
256
284
|
.limit(1);
|
|
257
285
|
if (!version) throw new WorkspaceArtifactNotFoundError("Artifact version not found");
|
|
258
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
artifactId,
|
|
288
|
+
version: versionFromRow(version),
|
|
289
|
+
contentKey: version.contentKey,
|
|
290
|
+
sourceKey: version.sourceKey,
|
|
291
|
+
};
|
|
259
292
|
});
|
|
260
293
|
}
|
|
261
294
|
|
|
@@ -265,6 +298,10 @@ type PublishMetadata = {
|
|
|
265
298
|
contentKey: string;
|
|
266
299
|
contentSha256: string;
|
|
267
300
|
sizeBytes: number;
|
|
301
|
+
sourceKey: string;
|
|
302
|
+
sourceSha256: string;
|
|
303
|
+
sourceSizeBytes: number;
|
|
304
|
+
requestedTools?: ToolGatewayIdentity[];
|
|
268
305
|
operationKey: string;
|
|
269
306
|
actorSubjectId: string;
|
|
270
307
|
sourceSessionId: string | null;
|
|
@@ -273,8 +310,70 @@ type PublishMetadata = {
|
|
|
273
310
|
sourceExecutionGeneration: number | null;
|
|
274
311
|
sourceToolName: ArtifactMutationToolName | null;
|
|
275
312
|
persistContent: () => Promise<void>;
|
|
313
|
+
discardContent: () => Promise<void>;
|
|
276
314
|
};
|
|
277
315
|
|
|
316
|
+
async function discardPersistedArtifactContent(
|
|
317
|
+
input: Pick<PublishMetadata, "discardContent">,
|
|
318
|
+
error: unknown,
|
|
319
|
+
): Promise<never> {
|
|
320
|
+
await input.discardContent().catch(() => undefined);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type ArtifactOperationReplay = NonNullable<Awaited<ReturnType<typeof replayForOperation>>>;
|
|
325
|
+
|
|
326
|
+
function persistedArtifactContentIsReferenced(
|
|
327
|
+
replay: ArtifactOperationReplay,
|
|
328
|
+
input: Pick<PublishMetadata, "contentKey" | "sourceKey">,
|
|
329
|
+
): boolean {
|
|
330
|
+
return (
|
|
331
|
+
replay.version.contentKey === input.contentKey || replay.version.sourceKey === input.sourceKey
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function reconcilePersistedArtifactMutation(
|
|
336
|
+
db: Database,
|
|
337
|
+
input: PublishMetadata,
|
|
338
|
+
error: unknown,
|
|
339
|
+
validateReplay: (replay: ArtifactOperationReplay) => void,
|
|
340
|
+
): Promise<WorkspaceArtifactMutationResponse> {
|
|
341
|
+
let replay: ArtifactOperationReplay | null;
|
|
342
|
+
try {
|
|
343
|
+
replay = await withRlsContext(
|
|
344
|
+
db,
|
|
345
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
346
|
+
async (scopedDb) => {
|
|
347
|
+
await lockOperation(scopedDb, input.workspaceId, input.operationKey);
|
|
348
|
+
return await replayForOperation(scopedDb, input.workspaceId, input.operationKey);
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
} catch {
|
|
352
|
+
// The transaction outcome is still unknown. Leaking an unreferenced object
|
|
353
|
+
// is recoverable; deleting content that may have committed is not.
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
if (!replay) return await discardPersistedArtifactContent(input, error);
|
|
357
|
+
try {
|
|
358
|
+
validateReplay(replay);
|
|
359
|
+
} catch (replayError) {
|
|
360
|
+
if (!persistedArtifactContentIsReferenced(replay, input)) {
|
|
361
|
+
await input.discardContent().catch(() => undefined);
|
|
362
|
+
}
|
|
363
|
+
throw replayError;
|
|
364
|
+
}
|
|
365
|
+
if (!persistedArtifactContentIsReferenced(replay, input)) {
|
|
366
|
+
await input.discardContent().catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
return mutationResult(
|
|
369
|
+
replay.artifact,
|
|
370
|
+
replay.version,
|
|
371
|
+
replay.event,
|
|
372
|
+
true,
|
|
373
|
+
replay.current ?? replay.version,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
278
377
|
async function assertAttemptAuthority(
|
|
279
378
|
scopedDb: any,
|
|
280
379
|
input: Pick<
|
|
@@ -368,6 +467,61 @@ async function assertAttemptAuthority(
|
|
|
368
467
|
}
|
|
369
468
|
}
|
|
370
469
|
|
|
470
|
+
async function assertArtifactRequestedToolsInAttemptCatalog(
|
|
471
|
+
scopedDb: any,
|
|
472
|
+
input: Pick<
|
|
473
|
+
PublishMetadata,
|
|
474
|
+
| "accountId"
|
|
475
|
+
| "workspaceId"
|
|
476
|
+
| "sourceSessionId"
|
|
477
|
+
| "sourceTurnId"
|
|
478
|
+
| "sourceAttemptId"
|
|
479
|
+
| "sourceExecutionGeneration"
|
|
480
|
+
>,
|
|
481
|
+
requestedTools: readonly ToolGatewayIdentity[],
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
if (input.sourceAttemptId === null || requestedTools.length === 0) return;
|
|
484
|
+
if (
|
|
485
|
+
input.sourceSessionId === null ||
|
|
486
|
+
input.sourceTurnId === null ||
|
|
487
|
+
input.sourceExecutionGeneration === null
|
|
488
|
+
) {
|
|
489
|
+
throw new WorkspaceArtifactOperationError("Artifact attempt provenance is incomplete");
|
|
490
|
+
}
|
|
491
|
+
const [row] = await scopedDb
|
|
492
|
+
.select({ catalog: schema.sessionAttemptToolCatalogs.catalog })
|
|
493
|
+
.from(schema.sessionAttemptToolCatalogs)
|
|
494
|
+
.where(
|
|
495
|
+
and(
|
|
496
|
+
eq(schema.sessionAttemptToolCatalogs.accountId, input.accountId),
|
|
497
|
+
eq(schema.sessionAttemptToolCatalogs.workspaceId, input.workspaceId),
|
|
498
|
+
eq(schema.sessionAttemptToolCatalogs.sessionId, input.sourceSessionId),
|
|
499
|
+
eq(schema.sessionAttemptToolCatalogs.turnId, input.sourceTurnId),
|
|
500
|
+
eq(schema.sessionAttemptToolCatalogs.attemptId, input.sourceAttemptId),
|
|
501
|
+
eq(schema.sessionAttemptToolCatalogs.executionGeneration, input.sourceExecutionGeneration),
|
|
502
|
+
),
|
|
503
|
+
)
|
|
504
|
+
.limit(1);
|
|
505
|
+
let allowed: Set<string>;
|
|
506
|
+
try {
|
|
507
|
+
const catalog = parseVerifiedAttemptToolCatalog(row?.catalog);
|
|
508
|
+
allowed = new Set(catalog.entries.map((entry) => toolIdentityKey(entry.identity)));
|
|
509
|
+
} catch {
|
|
510
|
+
throw new WorkspaceArtifactOperationError(
|
|
511
|
+
"Artifact requested tool authority is unavailable for the exact attempt",
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (requestedTools.some((identity) => !allowed.has(toolIdentityKey(identity)))) {
|
|
515
|
+
throw new WorkspaceArtifactOperationError(
|
|
516
|
+
"Artifact requested tools must be present in the exact attempt tool catalog",
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function toolIdentityKey(identity: ToolGatewayIdentity): string {
|
|
522
|
+
return `${identity.serverId}\u0000${identity.toolName}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
371
525
|
async function replayForOperation(scopedDb: any, workspaceId: string, operationKey: string) {
|
|
372
526
|
const [event] = await scopedDb
|
|
373
527
|
.select()
|
|
@@ -385,10 +539,35 @@ async function replayForOperation(scopedDb: any, workspaceId: string, operationK
|
|
|
385
539
|
const [version] = await scopedDb
|
|
386
540
|
.select()
|
|
387
541
|
.from(schema.workspaceArtifactVersions)
|
|
388
|
-
.where(
|
|
542
|
+
.where(
|
|
543
|
+
and(
|
|
544
|
+
eq(schema.workspaceArtifactVersions.workspaceId, workspaceId),
|
|
545
|
+
eq(schema.workspaceArtifactVersions.id, event.toVersionId),
|
|
546
|
+
),
|
|
547
|
+
)
|
|
389
548
|
.limit(1);
|
|
390
549
|
if (!version) return null;
|
|
391
|
-
|
|
550
|
+
let fromVersion: VersionRow | null = null;
|
|
551
|
+
if (event.fromVersionId) {
|
|
552
|
+
const [row] = await scopedDb
|
|
553
|
+
.select()
|
|
554
|
+
.from(schema.workspaceArtifactVersions)
|
|
555
|
+
.where(
|
|
556
|
+
and(
|
|
557
|
+
eq(schema.workspaceArtifactVersions.workspaceId, workspaceId),
|
|
558
|
+
eq(schema.workspaceArtifactVersions.id, event.fromVersionId),
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
.limit(1);
|
|
562
|
+
fromVersion = row ?? null;
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
artifact,
|
|
566
|
+
version,
|
|
567
|
+
fromVersion,
|
|
568
|
+
event,
|
|
569
|
+
current: await currentVersion(scopedDb, artifact),
|
|
570
|
+
} as const;
|
|
392
571
|
}
|
|
393
572
|
|
|
394
573
|
async function lockOperation(scopedDb: any, workspaceId: string, operationKey: string) {
|
|
@@ -410,6 +589,30 @@ function assertCreateReplay(replay: Awaited<ReturnType<typeof replayForOperation
|
|
|
410
589
|
}
|
|
411
590
|
}
|
|
412
591
|
|
|
592
|
+
function assertCreateReplayMatchesInput(
|
|
593
|
+
replay: ArtifactOperationReplay,
|
|
594
|
+
input: PublishMetadata & {
|
|
595
|
+
slug: string;
|
|
596
|
+
title: string;
|
|
597
|
+
description: string | null;
|
|
598
|
+
requestedSlug?: string | null;
|
|
599
|
+
},
|
|
600
|
+
): void {
|
|
601
|
+
assertCreateReplay(replay);
|
|
602
|
+
const requestDigest = createArtifactRequestDigest(input);
|
|
603
|
+
if (replay.event.requestDigest !== null && replay.event.requestDigest !== requestDigest) {
|
|
604
|
+
throw new WorkspaceArtifactConflictError(
|
|
605
|
+
"Idempotency key was already used with different artifact metadata",
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (replay.version.contentSha256 !== input.contentSha256) {
|
|
609
|
+
throw new WorkspaceArtifactConflictError(
|
|
610
|
+
"Idempotency key was already used with different content",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
assertReplayVersionMetadata(replay.version, input, input.requestedTools ?? []);
|
|
614
|
+
}
|
|
615
|
+
|
|
413
616
|
function assertPublishReplay(
|
|
414
617
|
replay: Awaited<ReturnType<typeof replayForOperation>>,
|
|
415
618
|
artifactId: string,
|
|
@@ -427,6 +630,83 @@ function assertPublishReplay(
|
|
|
427
630
|
}
|
|
428
631
|
}
|
|
429
632
|
|
|
633
|
+
function assertPublishReplayMatchesInput(
|
|
634
|
+
replay: ArtifactOperationReplay,
|
|
635
|
+
input: PublishMetadata & {
|
|
636
|
+
artifactId: string;
|
|
637
|
+
expectedCurrentVersionId: string;
|
|
638
|
+
},
|
|
639
|
+
): void {
|
|
640
|
+
assertPublishReplay(replay, input.artifactId, input.expectedCurrentVersionId);
|
|
641
|
+
const expectedRequestedTools = input.requestedTools ?? replay.fromVersion?.requestedTools;
|
|
642
|
+
if (!expectedRequestedTools) {
|
|
643
|
+
throw new WorkspaceArtifactConflictError(
|
|
644
|
+
"Idempotency key was already used for a publication with missing source authority",
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const requestDigest = publishArtifactRequestDigest(input, expectedRequestedTools);
|
|
648
|
+
if (replay.event.requestDigest !== null && replay.event.requestDigest !== requestDigest) {
|
|
649
|
+
throw new WorkspaceArtifactConflictError(
|
|
650
|
+
"Idempotency key was already used with different publication metadata",
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (replay.version.contentSha256 !== input.contentSha256) {
|
|
654
|
+
throw new WorkspaceArtifactConflictError(
|
|
655
|
+
"Idempotency key was already used with different content",
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
assertReplayVersionMetadata(replay.version, input, expectedRequestedTools);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function hashArtifactRequest(value: unknown): string {
|
|
662
|
+
return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function createArtifactRequestDigest(
|
|
666
|
+
input: PublishMetadata & {
|
|
667
|
+
slug: string;
|
|
668
|
+
title: string;
|
|
669
|
+
description: string | null;
|
|
670
|
+
requestedSlug?: string | null;
|
|
671
|
+
},
|
|
672
|
+
): string {
|
|
673
|
+
return hashArtifactRequest({
|
|
674
|
+
version: 1,
|
|
675
|
+
operation: "create",
|
|
676
|
+
requestedSlug: input.requestedSlug === undefined ? input.slug : input.requestedSlug,
|
|
677
|
+
title: input.title,
|
|
678
|
+
description: input.description,
|
|
679
|
+
contentSha256: input.contentSha256,
|
|
680
|
+
sourceSha256: input.sourceSha256,
|
|
681
|
+
requestedTools: input.requestedTools ?? [],
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function publishArtifactRequestDigest(
|
|
686
|
+
input: PublishMetadata & {
|
|
687
|
+
artifactId: string;
|
|
688
|
+
expectedCurrentVersionId: string;
|
|
689
|
+
title?: string;
|
|
690
|
+
description?: string | null;
|
|
691
|
+
},
|
|
692
|
+
requestedTools: readonly ToolGatewayIdentity[],
|
|
693
|
+
): string {
|
|
694
|
+
return hashArtifactRequest({
|
|
695
|
+
version: 1,
|
|
696
|
+
operation: "publish",
|
|
697
|
+
artifactId: input.artifactId,
|
|
698
|
+
expectedCurrentVersionId: input.expectedCurrentVersionId,
|
|
699
|
+
title: input.title === undefined ? { present: false } : { present: true, value: input.title },
|
|
700
|
+
description:
|
|
701
|
+
input.description === undefined
|
|
702
|
+
? { present: false }
|
|
703
|
+
: { present: true, value: input.description },
|
|
704
|
+
contentSha256: input.contentSha256,
|
|
705
|
+
sourceSha256: input.sourceSha256,
|
|
706
|
+
requestedTools,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
430
710
|
function assertRollbackReplay(
|
|
431
711
|
replay: Awaited<ReturnType<typeof replayForOperation>>,
|
|
432
712
|
artifactId: string,
|
|
@@ -448,6 +728,28 @@ function assertRollbackReplay(
|
|
|
448
728
|
}
|
|
449
729
|
}
|
|
450
730
|
|
|
731
|
+
function assertStatusReplay(
|
|
732
|
+
replay: Awaited<ReturnType<typeof replayForOperation>>,
|
|
733
|
+
artifactId: string,
|
|
734
|
+
status: "active" | "archived",
|
|
735
|
+
expectedCurrentVersionId: string,
|
|
736
|
+
reason: string,
|
|
737
|
+
) {
|
|
738
|
+
const expectedType = status === "archived" ? "archived" : "restored";
|
|
739
|
+
if (
|
|
740
|
+
!replay ||
|
|
741
|
+
replay.event.type !== expectedType ||
|
|
742
|
+
replay.event.artifactId !== artifactId ||
|
|
743
|
+
replay.event.fromVersionId !== expectedCurrentVersionId ||
|
|
744
|
+
replay.event.toVersionId !== expectedCurrentVersionId ||
|
|
745
|
+
replay.event.reason !== reason
|
|
746
|
+
) {
|
|
747
|
+
throw new WorkspaceArtifactConflictError(
|
|
748
|
+
"Idempotency key was already used for a different operation",
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
451
753
|
function mutationResult(
|
|
452
754
|
artifact: ArtifactRow,
|
|
453
755
|
version: VersionRow,
|
|
@@ -470,92 +772,105 @@ export async function createWorkspaceArtifact(
|
|
|
470
772
|
slug: string;
|
|
471
773
|
title: string;
|
|
472
774
|
description: string | null;
|
|
775
|
+
requestedSlug?: string | null;
|
|
473
776
|
},
|
|
474
777
|
): Promise<WorkspaceArtifactMutationResponse> {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
await
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
778
|
+
let contentPersisted = false;
|
|
779
|
+
try {
|
|
780
|
+
return await withRlsContext(
|
|
781
|
+
db,
|
|
782
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
783
|
+
async (scopedDb) => {
|
|
784
|
+
return await scopedDb.transaction(async (tx) => {
|
|
785
|
+
await lockOperation(tx, input.workspaceId, input.operationKey);
|
|
786
|
+
const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
|
|
787
|
+
if (replay) {
|
|
788
|
+
assertCreateReplayMatchesInput(replay, input);
|
|
789
|
+
return mutationResult(
|
|
790
|
+
replay.artifact,
|
|
791
|
+
replay.version,
|
|
792
|
+
replay.event,
|
|
793
|
+
true,
|
|
794
|
+
replay.current ?? replay.version,
|
|
488
795
|
);
|
|
489
796
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
797
|
+
await assertAttemptAuthority(tx, input);
|
|
798
|
+
await assertArtifactRequestedToolsInAttemptCatalog(tx, input, input.requestedTools ?? []);
|
|
799
|
+
await input.persistContent();
|
|
800
|
+
contentPersisted = true;
|
|
801
|
+
const [artifact] = await tx
|
|
802
|
+
.insert(schema.workspaceArtifacts)
|
|
803
|
+
.values({
|
|
804
|
+
id: input.artifactId,
|
|
805
|
+
accountId: input.accountId,
|
|
806
|
+
workspaceId: input.workspaceId,
|
|
807
|
+
slug: input.slug,
|
|
808
|
+
title: input.title,
|
|
809
|
+
description: input.description,
|
|
810
|
+
createdBySubjectId: input.actorSubjectId,
|
|
811
|
+
})
|
|
812
|
+
.returning();
|
|
813
|
+
const [version] = await tx
|
|
814
|
+
.insert(schema.workspaceArtifactVersions)
|
|
815
|
+
.values({
|
|
816
|
+
accountId: input.accountId,
|
|
817
|
+
workspaceId: input.workspaceId,
|
|
818
|
+
artifactId: input.artifactId,
|
|
819
|
+
revision: 1,
|
|
820
|
+
contentKey: input.contentKey,
|
|
821
|
+
contentSha256: input.contentSha256,
|
|
822
|
+
sizeBytes: input.sizeBytes,
|
|
823
|
+
sourceKey: input.sourceKey,
|
|
824
|
+
sourceSha256: input.sourceSha256,
|
|
825
|
+
sourceSizeBytes: input.sourceSizeBytes,
|
|
826
|
+
requestedTools: input.requestedTools ?? [],
|
|
827
|
+
operationKey: input.operationKey,
|
|
828
|
+
sourceSessionId: input.sourceSessionId,
|
|
829
|
+
sourceTurnId: input.sourceTurnId,
|
|
830
|
+
sourceAttemptId: input.sourceAttemptId,
|
|
831
|
+
sourceExecutionGeneration: input.sourceExecutionGeneration,
|
|
832
|
+
createdBySubjectId: input.actorSubjectId,
|
|
833
|
+
})
|
|
834
|
+
.returning();
|
|
835
|
+
const [updated] = await tx
|
|
836
|
+
.update(schema.workspaceArtifacts)
|
|
837
|
+
.set({
|
|
838
|
+
currentVersionId: version!.id,
|
|
839
|
+
updatedAt: new Date(),
|
|
840
|
+
})
|
|
841
|
+
.where(eq(schema.workspaceArtifacts.id, artifact!.id))
|
|
842
|
+
.returning();
|
|
843
|
+
const [event] = await tx
|
|
844
|
+
.insert(schema.workspaceArtifactEvents)
|
|
845
|
+
.values({
|
|
846
|
+
accountId: input.accountId,
|
|
847
|
+
workspaceId: input.workspaceId,
|
|
848
|
+
artifactId: input.artifactId,
|
|
849
|
+
type: "published",
|
|
850
|
+
fromVersionId: null,
|
|
851
|
+
toVersionId: version!.id,
|
|
852
|
+
operationKey: input.operationKey,
|
|
853
|
+
requestDigest: createArtifactRequestDigest(input),
|
|
854
|
+
sourceSessionId: input.sourceSessionId,
|
|
855
|
+
sourceTurnId: input.sourceTurnId,
|
|
856
|
+
sourceAttemptId: input.sourceAttemptId,
|
|
857
|
+
sourceExecutionGeneration: input.sourceExecutionGeneration,
|
|
858
|
+
actorSubjectId: input.actorSubjectId,
|
|
859
|
+
reason: "Initial publication",
|
|
860
|
+
})
|
|
861
|
+
.returning();
|
|
862
|
+
return mutationResult(updated!, version!, event!, false);
|
|
863
|
+
});
|
|
864
|
+
},
|
|
865
|
+
);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (contentPersisted) {
|
|
868
|
+
return await reconcilePersistedArtifactMutation(db, input, error, (replay) => {
|
|
869
|
+
assertCreateReplayMatchesInput(replay, input);
|
|
556
870
|
});
|
|
557
|
-
}
|
|
558
|
-
|
|
871
|
+
}
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
559
874
|
}
|
|
560
875
|
|
|
561
876
|
export async function publishWorkspaceArtifactVersion(
|
|
@@ -566,6 +881,146 @@ export async function publishWorkspaceArtifactVersion(
|
|
|
566
881
|
title?: string;
|
|
567
882
|
description?: string | null;
|
|
568
883
|
},
|
|
884
|
+
): Promise<WorkspaceArtifactMutationResponse> {
|
|
885
|
+
let contentPersisted = false;
|
|
886
|
+
try {
|
|
887
|
+
return await withRlsContext(
|
|
888
|
+
db,
|
|
889
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
890
|
+
async (scopedDb) => {
|
|
891
|
+
return await scopedDb.transaction(async (tx) => {
|
|
892
|
+
await lockOperation(tx, input.workspaceId, input.operationKey);
|
|
893
|
+
const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
|
|
894
|
+
if (replay) {
|
|
895
|
+
assertPublishReplayMatchesInput(replay, input);
|
|
896
|
+
return mutationResult(
|
|
897
|
+
replay.artifact,
|
|
898
|
+
replay.version,
|
|
899
|
+
replay.event,
|
|
900
|
+
true,
|
|
901
|
+
replay.current ?? replay.version,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
await assertAttemptAuthority(tx, input);
|
|
905
|
+
const artifact = await artifactRow(tx, input.workspaceId, input.artifactId, true);
|
|
906
|
+
if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
|
|
907
|
+
if (artifact.status !== "active") {
|
|
908
|
+
throw new WorkspaceArtifactOperationError("Archived artifacts cannot be published");
|
|
909
|
+
}
|
|
910
|
+
if (artifact.currentVersionId !== input.expectedCurrentVersionId) {
|
|
911
|
+
throw new WorkspaceArtifactConflictError(
|
|
912
|
+
"Artifact changed in another request",
|
|
913
|
+
artifact.currentVersionId,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
const current = await currentVersion(tx, artifact);
|
|
917
|
+
if (!current) throw new WorkspaceArtifactNotFoundError("Artifact has no current version");
|
|
918
|
+
await assertArtifactRequestedToolsInAttemptCatalog(
|
|
919
|
+
tx,
|
|
920
|
+
input,
|
|
921
|
+
input.requestedTools ?? current.requestedTools,
|
|
922
|
+
);
|
|
923
|
+
const [latest] = await tx
|
|
924
|
+
.select()
|
|
925
|
+
.from(schema.workspaceArtifactVersions)
|
|
926
|
+
.where(
|
|
927
|
+
and(
|
|
928
|
+
eq(schema.workspaceArtifactVersions.workspaceId, input.workspaceId),
|
|
929
|
+
eq(schema.workspaceArtifactVersions.artifactId, input.artifactId),
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
.orderBy(desc(schema.workspaceArtifactVersions.revision))
|
|
933
|
+
.limit(1);
|
|
934
|
+
await input.persistContent();
|
|
935
|
+
contentPersisted = true;
|
|
936
|
+
const [version] = await tx
|
|
937
|
+
.insert(schema.workspaceArtifactVersions)
|
|
938
|
+
.values({
|
|
939
|
+
accountId: input.accountId,
|
|
940
|
+
workspaceId: input.workspaceId,
|
|
941
|
+
artifactId: input.artifactId,
|
|
942
|
+
revision: (latest?.revision ?? 0) + 1,
|
|
943
|
+
contentKey: input.contentKey,
|
|
944
|
+
contentSha256: input.contentSha256,
|
|
945
|
+
sizeBytes: input.sizeBytes,
|
|
946
|
+
sourceKey: input.sourceKey,
|
|
947
|
+
sourceSha256: input.sourceSha256,
|
|
948
|
+
sourceSizeBytes: input.sourceSizeBytes,
|
|
949
|
+
requestedTools: input.requestedTools ?? current.requestedTools,
|
|
950
|
+
operationKey: input.operationKey,
|
|
951
|
+
sourceSessionId: input.sourceSessionId,
|
|
952
|
+
sourceTurnId: input.sourceTurnId,
|
|
953
|
+
sourceAttemptId: input.sourceAttemptId,
|
|
954
|
+
sourceExecutionGeneration: input.sourceExecutionGeneration,
|
|
955
|
+
createdBySubjectId: input.actorSubjectId,
|
|
956
|
+
})
|
|
957
|
+
.returning();
|
|
958
|
+
const update: Partial<typeof schema.workspaceArtifacts.$inferInsert> = {
|
|
959
|
+
currentVersionId: version!.id,
|
|
960
|
+
updatedAt: new Date(),
|
|
961
|
+
};
|
|
962
|
+
if (input.title !== undefined) update.title = input.title;
|
|
963
|
+
if (input.description !== undefined) update.description = input.description;
|
|
964
|
+
const [updated] = await tx
|
|
965
|
+
.update(schema.workspaceArtifacts)
|
|
966
|
+
.set(update)
|
|
967
|
+
.where(eq(schema.workspaceArtifacts.id, artifact.id))
|
|
968
|
+
.returning();
|
|
969
|
+
const [event] = await tx
|
|
970
|
+
.insert(schema.workspaceArtifactEvents)
|
|
971
|
+
.values({
|
|
972
|
+
accountId: input.accountId,
|
|
973
|
+
workspaceId: input.workspaceId,
|
|
974
|
+
artifactId: input.artifactId,
|
|
975
|
+
type: "published",
|
|
976
|
+
fromVersionId: artifact.currentVersionId,
|
|
977
|
+
toVersionId: version!.id,
|
|
978
|
+
operationKey: input.operationKey,
|
|
979
|
+
requestDigest: publishArtifactRequestDigest(
|
|
980
|
+
input,
|
|
981
|
+
input.requestedTools ?? current.requestedTools,
|
|
982
|
+
),
|
|
983
|
+
sourceSessionId: input.sourceSessionId,
|
|
984
|
+
sourceTurnId: input.sourceTurnId,
|
|
985
|
+
sourceAttemptId: input.sourceAttemptId,
|
|
986
|
+
sourceExecutionGeneration: input.sourceExecutionGeneration,
|
|
987
|
+
actorSubjectId: input.actorSubjectId,
|
|
988
|
+
reason: `Published revision ${version!.revision}`,
|
|
989
|
+
})
|
|
990
|
+
.returning();
|
|
991
|
+
return mutationResult(updated!, version!, event!, false);
|
|
992
|
+
});
|
|
993
|
+
},
|
|
994
|
+
);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (contentPersisted) {
|
|
997
|
+
return await reconcilePersistedArtifactMutation(db, input, error, (replay) => {
|
|
998
|
+
assertPublishReplayMatchesInput(replay, input);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export async function rollbackWorkspaceArtifact(
|
|
1006
|
+
db: Database,
|
|
1007
|
+
input: Omit<
|
|
1008
|
+
PublishMetadata,
|
|
1009
|
+
| "contentKey"
|
|
1010
|
+
| "contentSha256"
|
|
1011
|
+
| "sizeBytes"
|
|
1012
|
+
| "sourceKey"
|
|
1013
|
+
| "sourceSha256"
|
|
1014
|
+
| "sourceSizeBytes"
|
|
1015
|
+
| "requestedTools"
|
|
1016
|
+
| "persistContent"
|
|
1017
|
+
| "discardContent"
|
|
1018
|
+
> & {
|
|
1019
|
+
artifactId: string;
|
|
1020
|
+
versionId: string;
|
|
1021
|
+
expectedCurrentVersionId: string;
|
|
1022
|
+
reason: string;
|
|
1023
|
+
},
|
|
569
1024
|
): Promise<WorkspaceArtifactMutationResponse> {
|
|
570
1025
|
return await withRlsContext(
|
|
571
1026
|
db,
|
|
@@ -573,15 +1028,15 @@ export async function publishWorkspaceArtifactVersion(
|
|
|
573
1028
|
async (scopedDb) => {
|
|
574
1029
|
return await scopedDb.transaction(async (tx) => {
|
|
575
1030
|
await lockOperation(tx, input.workspaceId, input.operationKey);
|
|
576
|
-
await assertAttemptAuthority(tx, input);
|
|
577
1031
|
const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
|
|
578
1032
|
if (replay) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1033
|
+
assertRollbackReplay(
|
|
1034
|
+
replay,
|
|
1035
|
+
input.artifactId,
|
|
1036
|
+
input.versionId,
|
|
1037
|
+
input.expectedCurrentVersionId,
|
|
1038
|
+
input.reason,
|
|
1039
|
+
);
|
|
585
1040
|
return mutationResult(
|
|
586
1041
|
replay.artifact,
|
|
587
1042
|
replay.version,
|
|
@@ -590,56 +1045,33 @@ export async function publishWorkspaceArtifactVersion(
|
|
|
590
1045
|
replay.current ?? replay.version,
|
|
591
1046
|
);
|
|
592
1047
|
}
|
|
1048
|
+
await assertAttemptAuthority(tx, input);
|
|
593
1049
|
const artifact = await artifactRow(tx, input.workspaceId, input.artifactId, true);
|
|
594
1050
|
if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
|
|
595
1051
|
if (artifact.status !== "active") {
|
|
596
|
-
throw new WorkspaceArtifactOperationError("Archived artifacts cannot be
|
|
1052
|
+
throw new WorkspaceArtifactOperationError("Archived artifacts cannot be rolled back");
|
|
597
1053
|
}
|
|
598
|
-
if (artifact.currentVersionId !== input.expectedCurrentVersionId)
|
|
1054
|
+
if (artifact.currentVersionId !== input.expectedCurrentVersionId)
|
|
599
1055
|
throw new WorkspaceArtifactConflictError(
|
|
600
1056
|
"Artifact changed in another request",
|
|
601
1057
|
artifact.currentVersionId,
|
|
602
1058
|
);
|
|
603
|
-
|
|
604
|
-
const [latest] = await tx
|
|
1059
|
+
const [target] = await tx
|
|
605
1060
|
.select()
|
|
606
1061
|
.from(schema.workspaceArtifactVersions)
|
|
607
1062
|
.where(
|
|
608
1063
|
and(
|
|
609
1064
|
eq(schema.workspaceArtifactVersions.workspaceId, input.workspaceId),
|
|
610
1065
|
eq(schema.workspaceArtifactVersions.artifactId, input.artifactId),
|
|
1066
|
+
eq(schema.workspaceArtifactVersions.id, input.versionId),
|
|
611
1067
|
),
|
|
612
1068
|
)
|
|
613
|
-
.orderBy(desc(schema.workspaceArtifactVersions.revision))
|
|
614
1069
|
.limit(1);
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
.insert(schema.workspaceArtifactVersions)
|
|
618
|
-
.values({
|
|
619
|
-
accountId: input.accountId,
|
|
620
|
-
workspaceId: input.workspaceId,
|
|
621
|
-
artifactId: input.artifactId,
|
|
622
|
-
revision: (latest?.revision ?? 0) + 1,
|
|
623
|
-
contentKey: input.contentKey,
|
|
624
|
-
contentSha256: input.contentSha256,
|
|
625
|
-
sizeBytes: input.sizeBytes,
|
|
626
|
-
operationKey: input.operationKey,
|
|
627
|
-
sourceSessionId: input.sourceSessionId,
|
|
628
|
-
sourceTurnId: input.sourceTurnId,
|
|
629
|
-
sourceAttemptId: input.sourceAttemptId,
|
|
630
|
-
sourceExecutionGeneration: input.sourceExecutionGeneration,
|
|
631
|
-
createdBySubjectId: input.actorSubjectId,
|
|
632
|
-
})
|
|
633
|
-
.returning();
|
|
634
|
-
const update: Partial<typeof schema.workspaceArtifacts.$inferInsert> = {
|
|
635
|
-
currentVersionId: version!.id,
|
|
636
|
-
updatedAt: new Date(),
|
|
637
|
-
};
|
|
638
|
-
if (input.title !== undefined) update.title = input.title;
|
|
639
|
-
if (input.description !== undefined) update.description = input.description;
|
|
1070
|
+
if (!target) throw new WorkspaceArtifactNotFoundError("Artifact version not found");
|
|
1071
|
+
await assertArtifactRequestedToolsInAttemptCatalog(tx, input, target.requestedTools);
|
|
640
1072
|
const [updated] = await tx
|
|
641
1073
|
.update(schema.workspaceArtifacts)
|
|
642
|
-
.set(
|
|
1074
|
+
.set({ currentVersionId: target.id, updatedAt: new Date() })
|
|
643
1075
|
.where(eq(schema.workspaceArtifacts.id, artifact.id))
|
|
644
1076
|
.returning();
|
|
645
1077
|
const [event] = await tx
|
|
@@ -648,29 +1080,40 @@ export async function publishWorkspaceArtifactVersion(
|
|
|
648
1080
|
accountId: input.accountId,
|
|
649
1081
|
workspaceId: input.workspaceId,
|
|
650
1082
|
artifactId: input.artifactId,
|
|
651
|
-
type: "
|
|
1083
|
+
type: "rolled_back",
|
|
652
1084
|
fromVersionId: artifact.currentVersionId,
|
|
653
|
-
toVersionId:
|
|
1085
|
+
toVersionId: target.id,
|
|
654
1086
|
operationKey: input.operationKey,
|
|
655
1087
|
sourceSessionId: input.sourceSessionId,
|
|
656
1088
|
sourceTurnId: input.sourceTurnId,
|
|
657
1089
|
sourceAttemptId: input.sourceAttemptId,
|
|
658
1090
|
sourceExecutionGeneration: input.sourceExecutionGeneration,
|
|
659
1091
|
actorSubjectId: input.actorSubjectId,
|
|
660
|
-
reason:
|
|
1092
|
+
reason: input.reason,
|
|
661
1093
|
})
|
|
662
1094
|
.returning();
|
|
663
|
-
return mutationResult(updated!,
|
|
1095
|
+
return mutationResult(updated!, target, event!, false);
|
|
664
1096
|
});
|
|
665
1097
|
},
|
|
666
1098
|
);
|
|
667
1099
|
}
|
|
668
1100
|
|
|
669
|
-
export async function
|
|
1101
|
+
export async function setWorkspaceArtifactStatus(
|
|
670
1102
|
db: Database,
|
|
671
|
-
input: Omit<
|
|
1103
|
+
input: Omit<
|
|
1104
|
+
PublishMetadata,
|
|
1105
|
+
| "contentKey"
|
|
1106
|
+
| "contentSha256"
|
|
1107
|
+
| "sizeBytes"
|
|
1108
|
+
| "sourceKey"
|
|
1109
|
+
| "sourceSha256"
|
|
1110
|
+
| "sourceSizeBytes"
|
|
1111
|
+
| "requestedTools"
|
|
1112
|
+
| "persistContent"
|
|
1113
|
+
| "discardContent"
|
|
1114
|
+
> & {
|
|
672
1115
|
artifactId: string;
|
|
673
|
-
|
|
1116
|
+
status: "active" | "archived";
|
|
674
1117
|
expectedCurrentVersionId: string;
|
|
675
1118
|
reason: string;
|
|
676
1119
|
},
|
|
@@ -678,16 +1121,15 @@ export async function rollbackWorkspaceArtifact(
|
|
|
678
1121
|
return await withRlsContext(
|
|
679
1122
|
db,
|
|
680
1123
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
681
|
-
async (scopedDb) =>
|
|
682
|
-
|
|
1124
|
+
async (scopedDb) =>
|
|
1125
|
+
await scopedDb.transaction(async (tx) => {
|
|
683
1126
|
await lockOperation(tx, input.workspaceId, input.operationKey);
|
|
684
|
-
await assertAttemptAuthority(tx, input);
|
|
685
1127
|
const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
|
|
686
1128
|
if (replay) {
|
|
687
|
-
|
|
1129
|
+
assertStatusReplay(
|
|
688
1130
|
replay,
|
|
689
1131
|
input.artifactId,
|
|
690
|
-
input.
|
|
1132
|
+
input.status,
|
|
691
1133
|
input.expectedCurrentVersionId,
|
|
692
1134
|
input.reason,
|
|
693
1135
|
);
|
|
@@ -699,31 +1141,30 @@ export async function rollbackWorkspaceArtifact(
|
|
|
699
1141
|
replay.current ?? replay.version,
|
|
700
1142
|
);
|
|
701
1143
|
}
|
|
1144
|
+
await assertAttemptAuthority(tx, input);
|
|
702
1145
|
const artifact = await artifactRow(tx, input.workspaceId, input.artifactId, true);
|
|
703
1146
|
if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
|
|
704
|
-
if (artifact.
|
|
705
|
-
throw new WorkspaceArtifactOperationError("Archived artifacts cannot be rolled back");
|
|
706
|
-
}
|
|
707
|
-
if (artifact.currentVersionId !== input.expectedCurrentVersionId)
|
|
1147
|
+
if (artifact.currentVersionId !== input.expectedCurrentVersionId) {
|
|
708
1148
|
throw new WorkspaceArtifactConflictError(
|
|
709
1149
|
"Artifact changed in another request",
|
|
710
1150
|
artifact.currentVersionId,
|
|
711
1151
|
);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1152
|
+
}
|
|
1153
|
+
const current = await currentVersion(tx, artifact);
|
|
1154
|
+
if (!current) throw new WorkspaceArtifactNotFoundError("Artifact has no current version");
|
|
1155
|
+
if (input.status === "active") {
|
|
1156
|
+
await assertArtifactRequestedToolsInAttemptCatalog(tx, input, current.requestedTools);
|
|
1157
|
+
}
|
|
1158
|
+
if (artifact.status === input.status) {
|
|
1159
|
+
throw new WorkspaceArtifactOperationError(
|
|
1160
|
+
input.status === "archived"
|
|
1161
|
+
? "Artifact is already archived"
|
|
1162
|
+
: "Artifact is already active",
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
724
1165
|
const [updated] = await tx
|
|
725
1166
|
.update(schema.workspaceArtifacts)
|
|
726
|
-
.set({
|
|
1167
|
+
.set({ status: input.status, updatedAt: new Date() })
|
|
727
1168
|
.where(eq(schema.workspaceArtifacts.id, artifact.id))
|
|
728
1169
|
.returning();
|
|
729
1170
|
const [event] = await tx
|
|
@@ -732,9 +1173,9 @@ export async function rollbackWorkspaceArtifact(
|
|
|
732
1173
|
accountId: input.accountId,
|
|
733
1174
|
workspaceId: input.workspaceId,
|
|
734
1175
|
artifactId: input.artifactId,
|
|
735
|
-
type: "
|
|
736
|
-
fromVersionId:
|
|
737
|
-
toVersionId:
|
|
1176
|
+
type: input.status === "archived" ? "archived" : "restored",
|
|
1177
|
+
fromVersionId: current.id,
|
|
1178
|
+
toVersionId: current.id,
|
|
738
1179
|
operationKey: input.operationKey,
|
|
739
1180
|
sourceSessionId: input.sourceSessionId,
|
|
740
1181
|
sourceTurnId: input.sourceTurnId,
|
|
@@ -744,8 +1185,22 @@ export async function rollbackWorkspaceArtifact(
|
|
|
744
1185
|
reason: input.reason,
|
|
745
1186
|
})
|
|
746
1187
|
.returning();
|
|
747
|
-
return mutationResult(updated!,
|
|
748
|
-
})
|
|
749
|
-
},
|
|
1188
|
+
return mutationResult(updated!, current, event!, false);
|
|
1189
|
+
}),
|
|
750
1190
|
);
|
|
751
1191
|
}
|
|
1192
|
+
|
|
1193
|
+
function assertReplayVersionMetadata(
|
|
1194
|
+
version: VersionRow,
|
|
1195
|
+
input: PublishMetadata,
|
|
1196
|
+
expectedRequestedTools: ToolGatewayIdentity[],
|
|
1197
|
+
): void {
|
|
1198
|
+
if (
|
|
1199
|
+
version.sourceSha256 !== input.sourceSha256 ||
|
|
1200
|
+
JSON.stringify(version.requestedTools) !== JSON.stringify(expectedRequestedTools)
|
|
1201
|
+
) {
|
|
1202
|
+
throw new WorkspaceArtifactConflictError(
|
|
1203
|
+
"Idempotency key was already used with different source or requested tools",
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
}
|