@opengeni/db 0.22.2 → 0.26.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.
Files changed (55) hide show
  1. package/dist/{chunk-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
  2. package/dist/chunk-7WTDI7Y3.js.map +1 -0
  3. package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
  4. package/dist/chunk-KW6U54V2.js.map +1 -0
  5. package/dist/connection-token-resolver.d.ts +24 -2
  6. package/dist/index.d.ts +107 -5
  7. package/dist/index.js +7857 -3898
  8. package/dist/index.js.map +1 -1
  9. package/dist/preference-registry.d.ts +14 -0
  10. package/dist/provision-roles.js +1 -1
  11. package/dist/runtime-posture.d.ts +3 -3
  12. package/dist/schema.d.ts +1659 -77
  13. package/dist/schema.js +13 -1
  14. package/dist/session-control.d.ts +7 -1
  15. package/dist/session-queue-commands.d.ts +8 -0
  16. package/dist/session-realtime-context.d.ts +56 -0
  17. package/dist/session-realtime-ledger.d.ts +188 -0
  18. package/dist/session-realtime-mirror.d.ts +51 -0
  19. package/dist/session-realtime-state.d.ts +2 -0
  20. package/dist/session-realtime-terminal.d.ts +40 -0
  21. package/dist/session-realtime.d.ts +59 -0
  22. package/dist/workspace-instruction-policies-schema.d.ts +307 -0
  23. package/dist/workspace-instruction-policies.d.ts +97 -7
  24. package/drizzle/0156_slack_reaction_trigger.sql +49 -0
  25. package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
  26. package/drizzle/0158_session_realtime_mode.sql +88 -0
  27. package/drizzle/0159_session_realtime_ledger.sql +198 -0
  28. package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
  29. package/drizzle/0161_session_realtime_context_projection.sql +82 -0
  30. package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
  31. package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
  32. package/drizzle/0164_session_realtime_models.sql +28 -0
  33. package/drizzle/0165_document_authority_foundation.sql +259 -0
  34. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  35. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  36. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  37. package/package.json +4 -4
  38. package/src/connection-token-resolver.ts +79 -16
  39. package/src/index.ts +1033 -101
  40. package/src/preference-registry.ts +114 -6
  41. package/src/provision-roles.ts +12 -0
  42. package/src/runtime-posture.ts +12 -0
  43. package/src/schema.ts +486 -43
  44. package/src/session-control.ts +643 -21
  45. package/src/session-queue-commands.ts +121 -18
  46. package/src/session-realtime-context.ts +393 -0
  47. package/src/session-realtime-ledger.ts +1790 -0
  48. package/src/session-realtime-mirror.ts +276 -0
  49. package/src/session-realtime-state.ts +25 -0
  50. package/src/session-realtime-terminal.ts +306 -0
  51. package/src/session-realtime.ts +659 -0
  52. package/src/workspace-instruction-policies-schema.ts +71 -0
  53. package/src/workspace-instruction-policies.ts +568 -25
  54. package/dist/chunk-BNGEN5QZ.js.map +0 -1
  55. package/dist/chunk-CYGFLLMN.js.map +0 -1
@@ -1,5 +1,10 @@
1
- import { createHash } from "node:crypto";
2
- import { WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS } from "@opengeni/contracts";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ PreferenceRegistrySnapshot,
4
+ ResolvedWorkspaceInstructionPolicySnapshot,
5
+ WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
6
+ WorkspaceInstructionPolicySnapshot,
7
+ } from "@opengeni/contracts";
3
8
  import type {
4
9
  WorkspaceInstructionPolicyActivationEvent,
5
10
  WorkspaceInstructionPolicyActivationResponse,
@@ -13,14 +18,17 @@ import type {
13
18
  WorkspaceInstructionPolicyProvenanceSource,
14
19
  WorkspaceInstructionPolicyRevision,
15
20
  WorkspaceInstructionPolicyScope,
21
+ WorkspaceInstructionPolicySnapshotEntry,
16
22
  WorkspaceInstructionPolicyTarget,
17
23
  } from "@opengeni/contracts";
18
- import { and, asc, desc, eq, isNull, lt, type SQL } from "drizzle-orm";
24
+ import { and, asc, desc, eq, inArray, isNull, lt, or, sql, type SQL } from "drizzle-orm";
19
25
  import type { Database } from "./index";
20
- import { withRlsContext, withWorkspaceRls } from "./index";
26
+ import { withRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./index";
27
+ import { nestedPostgresSqlState } from "./persistence-errors";
21
28
  import * as schema from "./schema";
22
29
 
23
- type DraftInput = WorkspaceInstructionPolicyTarget & {
30
+ type DraftRequestInput = WorkspaceInstructionPolicyTarget & {
31
+ operationId: string;
24
32
  accountId: string;
25
33
  workspaceId: string;
26
34
  content: string;
@@ -30,10 +38,35 @@ type DraftInput = WorkspaceInstructionPolicyTarget & {
30
38
  createdBySubjectId: string;
31
39
  };
32
40
 
33
- type CallerDraftInput = Omit<DraftInput, "provenanceSource"> & {
41
+ type DraftInput = DraftRequestInput & {
42
+ requestFingerprint: string;
43
+ };
44
+
45
+ type CallerDraftInput = Omit<DraftRequestInput, "operationId" | "provenanceSource"> & {
46
+ operationId?: string;
34
47
  provenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource;
35
48
  };
36
49
 
50
+ type ImportLegacyRequestInput = {
51
+ operationId: string;
52
+ accountId: string;
53
+ workspaceId: string;
54
+ createdBySubjectId: string;
55
+ supersedesRevisionId: string | null;
56
+ };
57
+
58
+ type ActiveRevisionInput = {
59
+ operationId: string;
60
+ accountId: string;
61
+ workspaceId: string;
62
+ targetRevisionId: string;
63
+ expectedCurrentRevisionId: string | null;
64
+ expectedActivationVersion?: number;
65
+ actorSubjectId: string;
66
+ reason: string;
67
+ type: WorkspaceInstructionPolicyActivationType;
68
+ };
69
+
37
70
  export class WorkspaceInstructionPolicyConflictError extends Error {
38
71
  readonly name = "WorkspaceInstructionPolicyConflictError";
39
72
  readonly code = "WORKSPACE_INSTRUCTION_POLICY_CONFLICT";
@@ -43,6 +76,15 @@ export class WorkspaceInstructionPolicyConflictError extends Error {
43
76
  }
44
77
  }
45
78
 
79
+ export class WorkspaceInstructionPolicyOperationReuseError extends Error {
80
+ readonly name = "WorkspaceInstructionPolicyOperationReuseError";
81
+ readonly code = "WORKSPACE_INSTRUCTION_POLICY_OPERATION_REUSED";
82
+
83
+ constructor() {
84
+ super("The workspace instruction-policy operation id was already used for another request");
85
+ }
86
+ }
87
+
46
88
  export class WorkspaceInstructionPolicyNotFoundError extends Error {
47
89
  readonly name = "WorkspaceInstructionPolicyNotFoundError";
48
90
 
@@ -63,14 +105,244 @@ export class WorkspaceInstructionPolicyLegacyUnavailableError extends Error {
63
105
  }
64
106
  }
65
107
 
108
+ export class WorkspaceInstructionPolicySnapshotAuthorityError extends Error {
109
+ readonly name = "WorkspaceInstructionPolicySnapshotAuthorityError";
110
+ }
111
+
112
+ export type WorkspaceInstructionPolicyAttemptClaims = {
113
+ accountId: string;
114
+ workspaceId: string;
115
+ sessionId: string;
116
+ turnId: string;
117
+ attemptId: string;
118
+ executionGeneration: number;
119
+ };
120
+
121
+ export type WorkspaceStateAcceptedAttemptGovernance = {
122
+ attemptId: string;
123
+ executionGeneration: number;
124
+ acceptedAt: string;
125
+ policySnapshot: WorkspaceInstructionPolicySnapshot | null;
126
+ preferenceSnapshot: {
127
+ id: string;
128
+ descriptorHash: string;
129
+ descriptors: Array<{
130
+ id: string;
131
+ revisionId: string;
132
+ contentHash: string;
133
+ activeVersion: number;
134
+ scope: "organization" | "workspace" | "user";
135
+ }>;
136
+ truncated: boolean;
137
+ createdAt: string;
138
+ } | null;
139
+ };
140
+
66
141
  function contentHash(content: string): string {
67
142
  return createHash("sha256").update(content, "utf8").digest("hex");
68
143
  }
69
144
 
145
+ type OperationFingerprintField = readonly [
146
+ name: string,
147
+ present: boolean,
148
+ value: string | number | null,
149
+ ];
150
+
151
+ function hasOwnField(value: object, field: PropertyKey): boolean {
152
+ return Object.prototype.hasOwnProperty.call(value, field);
153
+ }
154
+
155
+ function operationRequestFingerprint(
156
+ operation: string,
157
+ fields: readonly OperationFingerprintField[],
158
+ ): string {
159
+ const canonicalRequest = JSON.stringify([
160
+ "workspace_instruction_policy_operation",
161
+ 1,
162
+ operation,
163
+ fields,
164
+ ]);
165
+ return createHash("sha256").update(canonicalRequest, "utf8").digest("hex");
166
+ }
167
+
168
+ function draftRequestFingerprint(input: DraftRequestInput): string {
169
+ return operationRequestFingerprint("create_draft", [
170
+ ["accountId", true, input.accountId],
171
+ ["workspaceId", true, input.workspaceId],
172
+ ["kind", true, input.kind],
173
+ ["scope", true, input.scope],
174
+ ["roleKey", true, input.roleKey],
175
+ ["content", true, input.content],
176
+ ["provenanceSource", true, input.provenanceSource],
177
+ ["provenanceSourceId", true, input.provenanceSourceId],
178
+ ["supersedesRevisionId", true, input.supersedesRevisionId],
179
+ ["createdBySubjectId", true, input.createdBySubjectId],
180
+ ]);
181
+ }
182
+
183
+ function importLegacyRequestFingerprint(input: ImportLegacyRequestInput): string {
184
+ return operationRequestFingerprint("import_legacy", [
185
+ ["accountId", true, input.accountId],
186
+ ["workspaceId", true, input.workspaceId],
187
+ ["createdBySubjectId", true, input.createdBySubjectId],
188
+ ["supersedesRevisionId", true, input.supersedesRevisionId],
189
+ ]);
190
+ }
191
+
192
+ function activeRevisionRequestFingerprint(input: ActiveRevisionInput): string {
193
+ const expectedCurrentRevisionIdPresent = hasOwnField(input, "expectedCurrentRevisionId");
194
+ const expectedActivationVersionPresent = hasOwnField(input, "expectedActivationVersion");
195
+ return operationRequestFingerprint(`change_active_revision:${input.type}`, [
196
+ ["accountId", true, input.accountId],
197
+ ["workspaceId", true, input.workspaceId],
198
+ ["targetRevisionId", true, input.targetRevisionId],
199
+ [
200
+ "expectedCurrentRevisionId",
201
+ expectedCurrentRevisionIdPresent,
202
+ input.expectedCurrentRevisionId ?? null,
203
+ ],
204
+ [
205
+ "expectedActivationVersion",
206
+ expectedActivationVersionPresent,
207
+ input.expectedActivationVersion ?? null,
208
+ ],
209
+ ["actorSubjectId", true, input.actorSubjectId],
210
+ ["reason", true, input.reason],
211
+ ]);
212
+ }
213
+
70
214
  function iso(value: Date | string): string {
71
215
  return (value instanceof Date ? value : new Date(value)).toISOString();
72
216
  }
73
217
 
218
+ /**
219
+ * Inspect one immutable accepted attempt only when the authenticated caller is
220
+ * exactly its frozen initiating human. The null result deliberately conflates
221
+ * absence, cross-account/workspace lookup, and another subject's attempt.
222
+ */
223
+ export async function getWorkspaceStateAcceptedAttemptGovernance(
224
+ db: Database,
225
+ input: { accountId: string; workspaceId: string; subjectId: string; attemptId: string },
226
+ ): Promise<WorkspaceStateAcceptedAttemptGovernance | null> {
227
+ return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
228
+ const initiatingSubject = sql<string>`coalesce(
229
+ ${schema.sessionTurns.initiatingHumanSubjectId},
230
+ case when ${schema.sessionTurns.initiatorKind} = 'subject'
231
+ then ${schema.sessionTurns.initiatorSubjectId}
232
+ end
233
+ )`;
234
+ const [attempt] = await scopedDb
235
+ .select({
236
+ attemptId: schema.sessionTurnAttempts.id,
237
+ executionGeneration: schema.sessionTurnAttempts.executionGeneration,
238
+ acceptedAt: schema.sessionTurns.createdAt,
239
+ })
240
+ .from(schema.sessionTurnAttempts)
241
+ .innerJoin(
242
+ schema.sessionTurns,
243
+ and(
244
+ eq(schema.sessionTurns.accountId, schema.sessionTurnAttempts.accountId),
245
+ eq(schema.sessionTurns.workspaceId, schema.sessionTurnAttempts.workspaceId),
246
+ eq(schema.sessionTurns.sessionId, schema.sessionTurnAttempts.sessionId),
247
+ eq(schema.sessionTurns.id, schema.sessionTurnAttempts.turnId),
248
+ ),
249
+ )
250
+ .where(
251
+ and(
252
+ eq(schema.sessionTurnAttempts.accountId, input.accountId),
253
+ eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
254
+ eq(schema.sessionTurnAttempts.id, input.attemptId),
255
+ eq(initiatingSubject, input.subjectId),
256
+ ),
257
+ )
258
+ .limit(1);
259
+ if (!attempt) return null;
260
+
261
+ const [policyRows, preferenceRows] = await Promise.all([
262
+ scopedDb
263
+ .select()
264
+ .from(schema.workspaceInstructionPolicySnapshots)
265
+ .where(
266
+ and(
267
+ eq(schema.workspaceInstructionPolicySnapshots.accountId, input.accountId),
268
+ eq(schema.workspaceInstructionPolicySnapshots.workspaceId, input.workspaceId),
269
+ eq(schema.workspaceInstructionPolicySnapshots.attemptId, input.attemptId),
270
+ eq(
271
+ schema.workspaceInstructionPolicySnapshots.executionGeneration,
272
+ attempt.executionGeneration,
273
+ ),
274
+ ),
275
+ )
276
+ .limit(1),
277
+ scopedDb
278
+ .select()
279
+ .from(schema.preferenceRegistrySnapshots)
280
+ .where(
281
+ and(
282
+ eq(schema.preferenceRegistrySnapshots.accountId, input.accountId),
283
+ eq(schema.preferenceRegistrySnapshots.workspaceId, input.workspaceId),
284
+ eq(schema.preferenceRegistrySnapshots.attemptId, input.attemptId),
285
+ eq(schema.preferenceRegistrySnapshots.executionGeneration, attempt.executionGeneration),
286
+ eq(schema.preferenceRegistrySnapshots.initiatingHumanSubjectId, input.subjectId),
287
+ ),
288
+ )
289
+ .limit(1),
290
+ ]);
291
+ const policyRow = policyRows[0] ?? null;
292
+ const preferenceRow = preferenceRows[0] ?? null;
293
+ return {
294
+ attemptId: attempt.attemptId,
295
+ executionGeneration: attempt.executionGeneration,
296
+ acceptedAt: iso(attempt.acceptedAt),
297
+ policySnapshot: policyRow
298
+ ? WorkspaceInstructionPolicySnapshot.parse({
299
+ id: policyRow.id,
300
+ workspaceId: policyRow.workspaceId,
301
+ sessionId: policyRow.sessionId,
302
+ turnId: policyRow.turnId,
303
+ attemptId: policyRow.attemptId,
304
+ executionGeneration: policyRow.executionGeneration,
305
+ policyRole: policyRow.policyRole,
306
+ roleSource: policyRow.roleSource,
307
+ entries: policyRow.entries,
308
+ entryHash: policyRow.entryHash,
309
+ createdAt: iso(policyRow.createdAt),
310
+ })
311
+ : null,
312
+ preferenceSnapshot: preferenceRow
313
+ ? (() => {
314
+ const snapshot = PreferenceRegistrySnapshot.parse({
315
+ id: preferenceRow.id,
316
+ workspaceId: preferenceRow.workspaceId,
317
+ sessionId: preferenceRow.sessionId,
318
+ turnId: preferenceRow.turnId,
319
+ attemptId: preferenceRow.attemptId,
320
+ executionGeneration: preferenceRow.executionGeneration,
321
+ initiatingHumanSubjectId: preferenceRow.initiatingHumanSubjectId,
322
+ descriptorHash: preferenceRow.descriptorHash,
323
+ descriptors: preferenceRow.descriptors,
324
+ truncated: preferenceRow.truncated,
325
+ createdAt: iso(preferenceRow.createdAt),
326
+ });
327
+ return {
328
+ id: snapshot.id,
329
+ descriptorHash: snapshot.descriptorHash,
330
+ descriptors: snapshot.descriptors.map((descriptor) => ({
331
+ id: descriptor.id,
332
+ revisionId: descriptor.revisionId,
333
+ contentHash: descriptor.contentHash,
334
+ activeVersion: descriptor.activeVersion,
335
+ scope: descriptor.scope,
336
+ })),
337
+ truncated: snapshot.truncated,
338
+ createdAt: snapshot.createdAt,
339
+ };
340
+ })()
341
+ : null,
342
+ };
343
+ });
344
+ }
345
+
74
346
  type RevisionRow = typeof schema.workspaceInstructionPolicyRevisions.$inferSelect;
75
347
  type HeadRow = typeof schema.workspaceInstructionPolicyHeads.$inferSelect;
76
348
  type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferSelect;
@@ -78,6 +350,7 @@ type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferS
78
350
  function revisionFromRow(row: RevisionRow): WorkspaceInstructionPolicyRevision {
79
351
  return {
80
352
  id: row.id,
353
+ operationId: row.operationId ?? row.id,
81
354
  accountId: row.accountId,
82
355
  workspaceId: row.workspaceId,
83
356
  revision: row.revision,
@@ -113,6 +386,7 @@ function headFromRow(row: HeadRow): WorkspaceInstructionPolicyHead {
113
386
  function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent {
114
387
  return {
115
388
  id: row.id,
389
+ operationId: row.operationId ?? row.id,
116
390
  accountId: row.accountId,
117
391
  workspaceId: row.workspaceId,
118
392
  kind: row.kind as WorkspaceInstructionPolicyKind,
@@ -139,6 +413,20 @@ function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent
139
413
  };
140
414
  }
141
415
 
416
+ function headFromEventRow(row: EventRow): WorkspaceInstructionPolicyHead {
417
+ return {
418
+ workspaceId: row.workspaceId,
419
+ kind: row.kind as WorkspaceInstructionPolicyKind,
420
+ scope: row.scope as WorkspaceInstructionPolicyScope,
421
+ roleKey: row.roleKey,
422
+ revisionId: row.newRevisionId,
423
+ revision: row.newRevision,
424
+ contentHash: row.newContentHash,
425
+ activationVersion: row.activationVersion,
426
+ activatedAt: iso(row.createdAt),
427
+ };
428
+ }
429
+
142
430
  function headTargetConditions(
143
431
  workspaceId: string,
144
432
  target: WorkspaceInstructionPolicyTarget,
@@ -206,6 +494,54 @@ async function getRevisionInTransaction(
206
494
  return row ?? null;
207
495
  }
208
496
 
497
+ async function getRevisionByOperationInTransaction(
498
+ db: Database,
499
+ workspaceId: string,
500
+ operationId: string,
501
+ ): Promise<RevisionRow | null> {
502
+ const [row] = await db
503
+ .select()
504
+ .from(schema.workspaceInstructionPolicyRevisions)
505
+ .where(
506
+ and(
507
+ eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
508
+ or(
509
+ eq(schema.workspaceInstructionPolicyRevisions.operationId, operationId),
510
+ and(
511
+ isNull(schema.workspaceInstructionPolicyRevisions.operationId),
512
+ eq(schema.workspaceInstructionPolicyRevisions.id, operationId),
513
+ ),
514
+ ),
515
+ ),
516
+ )
517
+ .limit(1);
518
+ return row ?? null;
519
+ }
520
+
521
+ async function getEventByOperationInTransaction(
522
+ db: Database,
523
+ workspaceId: string,
524
+ operationId: string,
525
+ ): Promise<EventRow | null> {
526
+ const [row] = await db
527
+ .select()
528
+ .from(schema.workspaceInstructionPolicyActivationEvents)
529
+ .where(
530
+ and(
531
+ eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId),
532
+ or(
533
+ eq(schema.workspaceInstructionPolicyActivationEvents.operationId, operationId),
534
+ and(
535
+ isNull(schema.workspaceInstructionPolicyActivationEvents.operationId),
536
+ eq(schema.workspaceInstructionPolicyActivationEvents.id, operationId),
537
+ ),
538
+ ),
539
+ ),
540
+ )
541
+ .limit(1);
542
+ return row ?? null;
543
+ }
544
+
209
545
  async function getHeadInTransaction(
210
546
  db: Database,
211
547
  workspaceId: string,
@@ -227,10 +563,41 @@ function sameTarget(
227
563
  return left.kind === right.kind && left.scope === right.scope && left.roleKey === right.roleKey;
228
564
  }
229
565
 
566
+ function draftReceiptMatches(row: RevisionRow, input: DraftInput): boolean {
567
+ return (
568
+ row.createdBySubjectId === input.createdBySubjectId &&
569
+ row.kind === input.kind &&
570
+ row.scope === input.scope &&
571
+ row.roleKey === input.roleKey &&
572
+ row.content === input.content &&
573
+ row.provenanceSource === input.provenanceSource &&
574
+ row.provenanceSourceId === input.provenanceSourceId &&
575
+ row.supersedesRevisionId === input.supersedesRevisionId
576
+ );
577
+ }
578
+
230
579
  async function createDraftInTransaction(
231
580
  db: Database,
232
581
  input: DraftInput,
233
582
  ): Promise<WorkspaceInstructionPolicyRevision> {
583
+ if (await getEventByOperationInTransaction(db, input.workspaceId, input.operationId)) {
584
+ throw new WorkspaceInstructionPolicyOperationReuseError();
585
+ }
586
+ const existing = await getRevisionByOperationInTransaction(
587
+ db,
588
+ input.workspaceId,
589
+ input.operationId,
590
+ );
591
+ if (existing) {
592
+ if (
593
+ existing.requestFingerprint === null
594
+ ? !draftReceiptMatches(existing, input)
595
+ : existing.requestFingerprint !== input.requestFingerprint
596
+ ) {
597
+ throw new WorkspaceInstructionPolicyOperationReuseError();
598
+ }
599
+ return revisionFromRow(existing);
600
+ }
234
601
  if (input.supersedesRevisionId !== null) {
235
602
  const superseded = await getRevisionInTransaction(
236
603
  db,
@@ -246,6 +613,8 @@ async function createDraftInTransaction(
246
613
  const [created] = await db
247
614
  .insert(schema.workspaceInstructionPolicyRevisions)
248
615
  .values({
616
+ operationId: input.operationId,
617
+ requestFingerprint: input.requestFingerprint,
249
618
  accountId: input.accountId,
250
619
  workspaceId: input.workspaceId,
251
620
  kind: input.kind,
@@ -267,12 +636,14 @@ export async function createWorkspaceInstructionPolicyDraft(
267
636
  db: Database,
268
637
  input: CallerDraftInput,
269
638
  ): Promise<WorkspaceInstructionPolicyRevision> {
639
+ const request = { ...input, operationId: input.operationId ?? randomUUID() };
640
+ const normalized = { ...request, requestFingerprint: draftRequestFingerprint(request) };
270
641
  return await withRlsContext(
271
642
  db,
272
643
  { accountId: input.accountId, workspaceId: input.workspaceId },
273
644
  async (scopedDb) => {
274
645
  await lockWorkspace(scopedDb, input);
275
- return await createDraftInTransaction(scopedDb, input);
646
+ return await createDraftInTransaction(scopedDb, normalized);
276
647
  },
277
648
  );
278
649
  }
@@ -280,18 +651,44 @@ export async function createWorkspaceInstructionPolicyDraft(
280
651
  /** Import only the stored legacy override; never materialize a deployment default or activate it. */
281
652
  export async function importLegacyWorkspaceInstructionPolicyDraft(
282
653
  db: Database,
283
- input: {
284
- accountId: string;
285
- workspaceId: string;
286
- createdBySubjectId: string;
287
- supersedesRevisionId: string | null;
288
- },
654
+ input: Omit<ImportLegacyRequestInput, "operationId"> & { operationId?: string },
289
655
  ): Promise<WorkspaceInstructionPolicyRevision> {
656
+ const request = { ...input, operationId: input.operationId ?? randomUUID() };
657
+ const normalized = {
658
+ ...request,
659
+ requestFingerprint: importLegacyRequestFingerprint(request),
660
+ };
290
661
  return await withRlsContext(
291
662
  db,
292
663
  { accountId: input.accountId, workspaceId: input.workspaceId },
293
664
  async (scopedDb) => {
294
665
  const workspace = await lockWorkspace(scopedDb, input);
666
+ if (
667
+ await getEventByOperationInTransaction(scopedDb, input.workspaceId, normalized.operationId)
668
+ ) {
669
+ throw new WorkspaceInstructionPolicyOperationReuseError();
670
+ }
671
+ const existing = await getRevisionByOperationInTransaction(
672
+ scopedDb,
673
+ input.workspaceId,
674
+ normalized.operationId,
675
+ );
676
+ if (existing) {
677
+ if (
678
+ existing.requestFingerprint === null
679
+ ? existing.createdBySubjectId !== input.createdBySubjectId ||
680
+ existing.kind !== "charter" ||
681
+ existing.scope !== "global" ||
682
+ existing.roleKey !== null ||
683
+ existing.provenanceSource !== "legacy_import" ||
684
+ existing.provenanceSourceId !== "workspaces.agent_instructions" ||
685
+ existing.supersedesRevisionId !== input.supersedesRevisionId
686
+ : existing.requestFingerprint !== normalized.requestFingerprint
687
+ ) {
688
+ throw new WorkspaceInstructionPolicyOperationReuseError();
689
+ }
690
+ return revisionFromRow(existing);
691
+ }
295
692
  if (workspace.agentInstructions === null) {
296
693
  throw new WorkspaceInstructionPolicyLegacyUnavailableError();
297
694
  }
@@ -304,7 +701,7 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
304
701
  );
305
702
  }
306
703
  return await createDraftInTransaction(scopedDb, {
307
- ...input,
704
+ ...normalized,
308
705
  kind: "charter",
309
706
  scope: "global",
310
707
  roleKey: null,
@@ -474,16 +871,9 @@ export async function diffWorkspaceInstructionPolicyRevisions(
474
871
 
475
872
  async function changeActiveRevision(
476
873
  db: Database,
477
- input: {
478
- accountId: string;
479
- workspaceId: string;
480
- targetRevisionId: string;
481
- expectedCurrentRevisionId: string | null;
482
- actorSubjectId: string;
483
- reason: string;
484
- type: WorkspaceInstructionPolicyActivationType;
485
- },
874
+ input: ActiveRevisionInput,
486
875
  ): Promise<WorkspaceInstructionPolicyActivationResponse> {
876
+ const requestFingerprint = activeRevisionRequestFingerprint(input);
487
877
  return await withRlsContext(
488
878
  db,
489
879
  { accountId: input.accountId, workspaceId: input.workspaceId },
@@ -491,6 +881,34 @@ async function changeActiveRevision(
491
881
  // This row lock serializes both an absent-head first activation and later
492
882
  // updates, so every loser can return the authoritative typed conflict.
493
883
  await lockWorkspace(scopedDb, input);
884
+ if (
885
+ await getRevisionByOperationInTransaction(scopedDb, input.workspaceId, input.operationId)
886
+ ) {
887
+ throw new WorkspaceInstructionPolicyOperationReuseError();
888
+ }
889
+ const existingEvent = await getEventByOperationInTransaction(
890
+ scopedDb,
891
+ input.workspaceId,
892
+ input.operationId,
893
+ );
894
+ if (existingEvent) {
895
+ const legacyRequestMatches =
896
+ hasOwnField(input, "expectedCurrentRevisionId") &&
897
+ !hasOwnField(input, "expectedActivationVersion") &&
898
+ existingEvent.type === input.type &&
899
+ existingEvent.newRevisionId === input.targetRevisionId &&
900
+ existingEvent.oldRevisionId === input.expectedCurrentRevisionId &&
901
+ existingEvent.actorSubjectId === input.actorSubjectId &&
902
+ existingEvent.reason === input.reason;
903
+ if (
904
+ existingEvent.requestFingerprint === null
905
+ ? !legacyRequestMatches
906
+ : existingEvent.requestFingerprint !== requestFingerprint
907
+ ) {
908
+ throw new WorkspaceInstructionPolicyOperationReuseError();
909
+ }
910
+ return { head: headFromEventRow(existingEvent), event: eventFromRow(existingEvent) };
911
+ }
494
912
  const targetRow = await getRevisionInTransaction(
495
913
  scopedDb,
496
914
  input.workspaceId,
@@ -505,7 +923,11 @@ async function changeActiveRevision(
505
923
  };
506
924
  const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, targetIdentity);
507
925
  const currentHead = currentRow ? headFromRow(currentRow) : null;
508
- if ((currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId) {
926
+ if (
927
+ (currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
928
+ (input.expectedActivationVersion !== undefined &&
929
+ (currentHead?.activationVersion ?? 0) !== input.expectedActivationVersion)
930
+ ) {
509
931
  throw new WorkspaceInstructionPolicyConflictError(currentHead);
510
932
  }
511
933
  if (currentHead?.revisionId === target.id) {
@@ -540,6 +962,8 @@ async function changeActiveRevision(
540
962
  const [eventRow] = await scopedDb
541
963
  .insert(schema.workspaceInstructionPolicyActivationEvents)
542
964
  .values({
965
+ operationId: input.operationId,
966
+ requestFingerprint,
543
967
  accountId: input.accountId,
544
968
  workspaceId: input.workspaceId,
545
969
  ...targetIdentity,
@@ -594,16 +1018,19 @@ async function changeActiveRevision(
594
1018
  export async function activateWorkspaceInstructionPolicyRevision(
595
1019
  db: Database,
596
1020
  input: {
1021
+ operationId?: string;
597
1022
  accountId: string;
598
1023
  workspaceId: string;
599
1024
  revisionId: string;
600
1025
  expectedCurrentRevisionId: string | null;
1026
+ expectedActivationVersion?: number;
601
1027
  actorSubjectId: string;
602
1028
  reason: string;
603
1029
  },
604
1030
  ): Promise<WorkspaceInstructionPolicyActivationResponse> {
605
1031
  return await changeActiveRevision(db, {
606
1032
  ...input,
1033
+ operationId: input.operationId ?? randomUUID(),
607
1034
  targetRevisionId: input.revisionId,
608
1035
  type: "activate",
609
1036
  });
@@ -612,13 +1039,129 @@ export async function activateWorkspaceInstructionPolicyRevision(
612
1039
  export async function rollbackWorkspaceInstructionPolicyRevision(
613
1040
  db: Database,
614
1041
  input: {
1042
+ operationId?: string;
615
1043
  accountId: string;
616
1044
  workspaceId: string;
617
1045
  targetRevisionId: string;
618
1046
  expectedCurrentRevisionId: string;
1047
+ expectedActivationVersion?: number;
619
1048
  actorSubjectId: string;
620
1049
  reason: string;
621
1050
  },
622
1051
  ): Promise<WorkspaceInstructionPolicyActivationResponse> {
623
- return await changeActiveRevision(db, { ...input, type: "rollback" });
1052
+ return await changeActiveRevision(db, {
1053
+ ...input,
1054
+ operationId: input.operationId ?? randomUUID(),
1055
+ type: "rollback",
1056
+ });
1057
+ }
1058
+
1059
+ /**
1060
+ * Freeze (or replay) the exact active charter/global/role policy set for one
1061
+ * accepted attempt, then resolve only those immutable revisions. The snapshot
1062
+ * stores bounded descriptors/hashes; full policy text remains in the revision
1063
+ * table and cannot be widened by a later head activation.
1064
+ */
1065
+ export async function getOrCreateWorkspaceInstructionPolicySnapshot(
1066
+ db: Database,
1067
+ claims: WorkspaceInstructionPolicyAttemptClaims,
1068
+ ) {
1069
+ try {
1070
+ return await withWorkspaceRls(db, claims.workspaceId, async (scopedDb) => {
1071
+ const rows = (await scopedDb.execute(sql`
1072
+ SELECT
1073
+ snapshot.id,
1074
+ snapshot.workspace_id AS "workspaceId",
1075
+ snapshot.session_id AS "sessionId",
1076
+ snapshot.turn_id AS "turnId",
1077
+ snapshot.attempt_id AS "attemptId",
1078
+ snapshot.execution_generation AS "executionGeneration",
1079
+ snapshot.policy_role AS "policyRole",
1080
+ snapshot.role_source AS "roleSource",
1081
+ snapshot.entries,
1082
+ snapshot.entry_hash AS "entryHash",
1083
+ snapshot.created_at AS "createdAt"
1084
+ FROM workspace_instruction_policy_get_or_create_snapshot(
1085
+ ${claims.accountId}::uuid,
1086
+ ${claims.workspaceId}::uuid,
1087
+ ${claims.sessionId}::uuid,
1088
+ ${claims.turnId}::uuid,
1089
+ ${claims.attemptId}::uuid,
1090
+ ${claims.executionGeneration}
1091
+ ) snapshot
1092
+ `)) as unknown as Array<{
1093
+ id: string;
1094
+ workspaceId: string;
1095
+ sessionId: string;
1096
+ turnId: string;
1097
+ attemptId: string;
1098
+ executionGeneration: number;
1099
+ policyRole: string | null;
1100
+ roleSource: string;
1101
+ entries: unknown;
1102
+ entryHash: string;
1103
+ createdAt: Date | string;
1104
+ }>;
1105
+ const row = rows[0];
1106
+ if (!row) {
1107
+ throw new WorkspaceInstructionPolicySnapshotAuthorityError(
1108
+ "Instruction-policy snapshot authority conflicts with the accepted attempt",
1109
+ );
1110
+ }
1111
+ const snapshot = WorkspaceInstructionPolicySnapshot.parse({
1112
+ ...row,
1113
+ createdAt: iso(row.createdAt),
1114
+ });
1115
+ if (snapshot.entries.length === 0) {
1116
+ return ResolvedWorkspaceInstructionPolicySnapshot.parse(snapshot);
1117
+ }
1118
+
1119
+ const revisionIds = snapshot.entries.map((entry) => entry.revisionId);
1120
+ const revisions = await scopedDb
1121
+ .select()
1122
+ .from(schema.workspaceInstructionPolicyRevisions)
1123
+ .where(
1124
+ and(
1125
+ eq(schema.workspaceInstructionPolicyRevisions.accountId, claims.accountId),
1126
+ eq(schema.workspaceInstructionPolicyRevisions.workspaceId, claims.workspaceId),
1127
+ inArray(schema.workspaceInstructionPolicyRevisions.id, revisionIds),
1128
+ ),
1129
+ );
1130
+ const revisionsById = new Map(revisions.map((revision) => [revision.id, revision]));
1131
+ const resolvedEntries = snapshot.entries.map((entry) => {
1132
+ const revision = revisionsById.get(entry.revisionId);
1133
+ if (!revision || !snapshotEntryMatchesRevision(entry, revision)) {
1134
+ throw new WorkspaceInstructionPolicyInvalidOperationError(
1135
+ "Instruction-policy snapshot references an inexact immutable revision",
1136
+ );
1137
+ }
1138
+ return { ...entry, content: revision.content };
1139
+ });
1140
+ return ResolvedWorkspaceInstructionPolicySnapshot.parse({
1141
+ ...snapshot,
1142
+ entries: resolvedEntries,
1143
+ });
1144
+ });
1145
+ } catch (error) {
1146
+ if (["40001", "42501"].includes(nestedPostgresSqlState(error) ?? "")) {
1147
+ throw new WorkspaceInstructionPolicySnapshotAuthorityError(
1148
+ "Instruction-policy snapshot requires the exact current attempt and generation",
1149
+ );
1150
+ }
1151
+ throw error;
1152
+ }
1153
+ }
1154
+
1155
+ function snapshotEntryMatchesRevision(
1156
+ entry: WorkspaceInstructionPolicySnapshotEntry,
1157
+ revision: RevisionRow,
1158
+ ): boolean {
1159
+ return (
1160
+ revision.id === entry.revisionId &&
1161
+ revision.revision === entry.revision &&
1162
+ revision.contentHash === entry.contentHash &&
1163
+ revision.kind === entry.kind &&
1164
+ revision.scope === entry.scope &&
1165
+ revision.roleKey === entry.roleKey
1166
+ );
624
1167
  }