@opengeni/db 0.23.0 → 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.
- package/dist/{chunk-3UCHDMKG.js → chunk-7WTDI7Y3.js} +109 -2
- package/dist/chunk-7WTDI7Y3.js.map +1 -0
- package/dist/{chunk-L6ADMZHE.js → chunk-KW6U54V2.js} +3 -1
- package/dist/chunk-KW6U54V2.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +1168 -336
- package/dist/index.js.map +1 -1
- package/dist/preference-registry.d.ts +14 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +262 -4
- package/dist/schema.js +3 -1
- package/dist/session-realtime-mirror.d.ts +22 -1
- package/dist/workspace-instruction-policies-schema.d.ts +68 -0
- package/dist/workspace-instruction-policies.d.ts +53 -7
- package/drizzle/0165_document_authority_foundation.sql +259 -0
- package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
- package/drizzle/0167_document_index_replay_authority.sql +61 -0
- package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
- package/package.json +3 -3
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +419 -23
- package/src/preference-registry.ts +103 -0
- package/src/runtime-posture.ts +2 -0
- package/src/schema.ts +86 -0
- package/src/session-control.ts +48 -1
- package/src/session-queue-commands.ts +45 -11
- package/src/session-realtime-mirror.ts +117 -1
- package/src/session-realtime.ts +49 -1
- package/src/workspace-instruction-policies-schema.ts +30 -0
- package/src/workspace-instruction-policies.ts +438 -24
- package/dist/chunk-3UCHDMKG.js.map +0 -1
- package/dist/chunk-L6ADMZHE.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
PreferenceRegistrySnapshot,
|
|
3
4
|
ResolvedWorkspaceInstructionPolicySnapshot,
|
|
4
5
|
WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
|
|
5
6
|
WorkspaceInstructionPolicySnapshot,
|
|
@@ -20,13 +21,14 @@ import type {
|
|
|
20
21
|
WorkspaceInstructionPolicySnapshotEntry,
|
|
21
22
|
WorkspaceInstructionPolicyTarget,
|
|
22
23
|
} from "@opengeni/contracts";
|
|
23
|
-
import { and, asc, desc, eq, inArray, isNull, lt, sql, type SQL } from "drizzle-orm";
|
|
24
|
+
import { and, asc, desc, eq, inArray, isNull, lt, or, sql, type SQL } from "drizzle-orm";
|
|
24
25
|
import type { Database } from "./index";
|
|
25
|
-
import { withRlsContext, withWorkspaceRls } from "./index";
|
|
26
|
+
import { withRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./index";
|
|
26
27
|
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
27
28
|
import * as schema from "./schema";
|
|
28
29
|
|
|
29
|
-
type
|
|
30
|
+
type DraftRequestInput = WorkspaceInstructionPolicyTarget & {
|
|
31
|
+
operationId: string;
|
|
30
32
|
accountId: string;
|
|
31
33
|
workspaceId: string;
|
|
32
34
|
content: string;
|
|
@@ -36,10 +38,35 @@ type DraftInput = WorkspaceInstructionPolicyTarget & {
|
|
|
36
38
|
createdBySubjectId: string;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
type
|
|
41
|
+
type DraftInput = DraftRequestInput & {
|
|
42
|
+
requestFingerprint: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type CallerDraftInput = Omit<DraftRequestInput, "operationId" | "provenanceSource"> & {
|
|
46
|
+
operationId?: string;
|
|
40
47
|
provenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource;
|
|
41
48
|
};
|
|
42
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
|
+
|
|
43
70
|
export class WorkspaceInstructionPolicyConflictError extends Error {
|
|
44
71
|
readonly name = "WorkspaceInstructionPolicyConflictError";
|
|
45
72
|
readonly code = "WORKSPACE_INSTRUCTION_POLICY_CONFLICT";
|
|
@@ -49,6 +76,15 @@ export class WorkspaceInstructionPolicyConflictError extends Error {
|
|
|
49
76
|
}
|
|
50
77
|
}
|
|
51
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
|
+
|
|
52
88
|
export class WorkspaceInstructionPolicyNotFoundError extends Error {
|
|
53
89
|
readonly name = "WorkspaceInstructionPolicyNotFoundError";
|
|
54
90
|
|
|
@@ -82,14 +118,231 @@ export type WorkspaceInstructionPolicyAttemptClaims = {
|
|
|
82
118
|
executionGeneration: number;
|
|
83
119
|
};
|
|
84
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
|
+
|
|
85
141
|
function contentHash(content: string): string {
|
|
86
142
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
87
143
|
}
|
|
88
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
|
+
|
|
89
214
|
function iso(value: Date | string): string {
|
|
90
215
|
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
91
216
|
}
|
|
92
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
|
+
|
|
93
346
|
type RevisionRow = typeof schema.workspaceInstructionPolicyRevisions.$inferSelect;
|
|
94
347
|
type HeadRow = typeof schema.workspaceInstructionPolicyHeads.$inferSelect;
|
|
95
348
|
type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferSelect;
|
|
@@ -97,6 +350,7 @@ type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferS
|
|
|
97
350
|
function revisionFromRow(row: RevisionRow): WorkspaceInstructionPolicyRevision {
|
|
98
351
|
return {
|
|
99
352
|
id: row.id,
|
|
353
|
+
operationId: row.operationId ?? row.id,
|
|
100
354
|
accountId: row.accountId,
|
|
101
355
|
workspaceId: row.workspaceId,
|
|
102
356
|
revision: row.revision,
|
|
@@ -132,6 +386,7 @@ function headFromRow(row: HeadRow): WorkspaceInstructionPolicyHead {
|
|
|
132
386
|
function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent {
|
|
133
387
|
return {
|
|
134
388
|
id: row.id,
|
|
389
|
+
operationId: row.operationId ?? row.id,
|
|
135
390
|
accountId: row.accountId,
|
|
136
391
|
workspaceId: row.workspaceId,
|
|
137
392
|
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
@@ -158,6 +413,20 @@ function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent
|
|
|
158
413
|
};
|
|
159
414
|
}
|
|
160
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
|
+
|
|
161
430
|
function headTargetConditions(
|
|
162
431
|
workspaceId: string,
|
|
163
432
|
target: WorkspaceInstructionPolicyTarget,
|
|
@@ -225,6 +494,54 @@ async function getRevisionInTransaction(
|
|
|
225
494
|
return row ?? null;
|
|
226
495
|
}
|
|
227
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
|
+
|
|
228
545
|
async function getHeadInTransaction(
|
|
229
546
|
db: Database,
|
|
230
547
|
workspaceId: string,
|
|
@@ -246,10 +563,41 @@ function sameTarget(
|
|
|
246
563
|
return left.kind === right.kind && left.scope === right.scope && left.roleKey === right.roleKey;
|
|
247
564
|
}
|
|
248
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
|
+
|
|
249
579
|
async function createDraftInTransaction(
|
|
250
580
|
db: Database,
|
|
251
581
|
input: DraftInput,
|
|
252
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
|
+
}
|
|
253
601
|
if (input.supersedesRevisionId !== null) {
|
|
254
602
|
const superseded = await getRevisionInTransaction(
|
|
255
603
|
db,
|
|
@@ -265,6 +613,8 @@ async function createDraftInTransaction(
|
|
|
265
613
|
const [created] = await db
|
|
266
614
|
.insert(schema.workspaceInstructionPolicyRevisions)
|
|
267
615
|
.values({
|
|
616
|
+
operationId: input.operationId,
|
|
617
|
+
requestFingerprint: input.requestFingerprint,
|
|
268
618
|
accountId: input.accountId,
|
|
269
619
|
workspaceId: input.workspaceId,
|
|
270
620
|
kind: input.kind,
|
|
@@ -286,12 +636,14 @@ export async function createWorkspaceInstructionPolicyDraft(
|
|
|
286
636
|
db: Database,
|
|
287
637
|
input: CallerDraftInput,
|
|
288
638
|
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
639
|
+
const request = { ...input, operationId: input.operationId ?? randomUUID() };
|
|
640
|
+
const normalized = { ...request, requestFingerprint: draftRequestFingerprint(request) };
|
|
289
641
|
return await withRlsContext(
|
|
290
642
|
db,
|
|
291
643
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
292
644
|
async (scopedDb) => {
|
|
293
645
|
await lockWorkspace(scopedDb, input);
|
|
294
|
-
return await createDraftInTransaction(scopedDb,
|
|
646
|
+
return await createDraftInTransaction(scopedDb, normalized);
|
|
295
647
|
},
|
|
296
648
|
);
|
|
297
649
|
}
|
|
@@ -299,18 +651,44 @@ export async function createWorkspaceInstructionPolicyDraft(
|
|
|
299
651
|
/** Import only the stored legacy override; never materialize a deployment default or activate it. */
|
|
300
652
|
export async function importLegacyWorkspaceInstructionPolicyDraft(
|
|
301
653
|
db: Database,
|
|
302
|
-
input: {
|
|
303
|
-
accountId: string;
|
|
304
|
-
workspaceId: string;
|
|
305
|
-
createdBySubjectId: string;
|
|
306
|
-
supersedesRevisionId: string | null;
|
|
307
|
-
},
|
|
654
|
+
input: Omit<ImportLegacyRequestInput, "operationId"> & { operationId?: string },
|
|
308
655
|
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
656
|
+
const request = { ...input, operationId: input.operationId ?? randomUUID() };
|
|
657
|
+
const normalized = {
|
|
658
|
+
...request,
|
|
659
|
+
requestFingerprint: importLegacyRequestFingerprint(request),
|
|
660
|
+
};
|
|
309
661
|
return await withRlsContext(
|
|
310
662
|
db,
|
|
311
663
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
312
664
|
async (scopedDb) => {
|
|
313
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
|
+
}
|
|
314
692
|
if (workspace.agentInstructions === null) {
|
|
315
693
|
throw new WorkspaceInstructionPolicyLegacyUnavailableError();
|
|
316
694
|
}
|
|
@@ -323,7 +701,7 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
|
|
|
323
701
|
);
|
|
324
702
|
}
|
|
325
703
|
return await createDraftInTransaction(scopedDb, {
|
|
326
|
-
...
|
|
704
|
+
...normalized,
|
|
327
705
|
kind: "charter",
|
|
328
706
|
scope: "global",
|
|
329
707
|
roleKey: null,
|
|
@@ -493,16 +871,9 @@ export async function diffWorkspaceInstructionPolicyRevisions(
|
|
|
493
871
|
|
|
494
872
|
async function changeActiveRevision(
|
|
495
873
|
db: Database,
|
|
496
|
-
input:
|
|
497
|
-
accountId: string;
|
|
498
|
-
workspaceId: string;
|
|
499
|
-
targetRevisionId: string;
|
|
500
|
-
expectedCurrentRevisionId: string | null;
|
|
501
|
-
actorSubjectId: string;
|
|
502
|
-
reason: string;
|
|
503
|
-
type: WorkspaceInstructionPolicyActivationType;
|
|
504
|
-
},
|
|
874
|
+
input: ActiveRevisionInput,
|
|
505
875
|
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
876
|
+
const requestFingerprint = activeRevisionRequestFingerprint(input);
|
|
506
877
|
return await withRlsContext(
|
|
507
878
|
db,
|
|
508
879
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
@@ -510,6 +881,34 @@ async function changeActiveRevision(
|
|
|
510
881
|
// This row lock serializes both an absent-head first activation and later
|
|
511
882
|
// updates, so every loser can return the authoritative typed conflict.
|
|
512
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
|
+
}
|
|
513
912
|
const targetRow = await getRevisionInTransaction(
|
|
514
913
|
scopedDb,
|
|
515
914
|
input.workspaceId,
|
|
@@ -524,7 +923,11 @@ async function changeActiveRevision(
|
|
|
524
923
|
};
|
|
525
924
|
const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, targetIdentity);
|
|
526
925
|
const currentHead = currentRow ? headFromRow(currentRow) : null;
|
|
527
|
-
if (
|
|
926
|
+
if (
|
|
927
|
+
(currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
|
|
928
|
+
(input.expectedActivationVersion !== undefined &&
|
|
929
|
+
(currentHead?.activationVersion ?? 0) !== input.expectedActivationVersion)
|
|
930
|
+
) {
|
|
528
931
|
throw new WorkspaceInstructionPolicyConflictError(currentHead);
|
|
529
932
|
}
|
|
530
933
|
if (currentHead?.revisionId === target.id) {
|
|
@@ -559,6 +962,8 @@ async function changeActiveRevision(
|
|
|
559
962
|
const [eventRow] = await scopedDb
|
|
560
963
|
.insert(schema.workspaceInstructionPolicyActivationEvents)
|
|
561
964
|
.values({
|
|
965
|
+
operationId: input.operationId,
|
|
966
|
+
requestFingerprint,
|
|
562
967
|
accountId: input.accountId,
|
|
563
968
|
workspaceId: input.workspaceId,
|
|
564
969
|
...targetIdentity,
|
|
@@ -613,16 +1018,19 @@ async function changeActiveRevision(
|
|
|
613
1018
|
export async function activateWorkspaceInstructionPolicyRevision(
|
|
614
1019
|
db: Database,
|
|
615
1020
|
input: {
|
|
1021
|
+
operationId?: string;
|
|
616
1022
|
accountId: string;
|
|
617
1023
|
workspaceId: string;
|
|
618
1024
|
revisionId: string;
|
|
619
1025
|
expectedCurrentRevisionId: string | null;
|
|
1026
|
+
expectedActivationVersion?: number;
|
|
620
1027
|
actorSubjectId: string;
|
|
621
1028
|
reason: string;
|
|
622
1029
|
},
|
|
623
1030
|
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
624
1031
|
return await changeActiveRevision(db, {
|
|
625
1032
|
...input,
|
|
1033
|
+
operationId: input.operationId ?? randomUUID(),
|
|
626
1034
|
targetRevisionId: input.revisionId,
|
|
627
1035
|
type: "activate",
|
|
628
1036
|
});
|
|
@@ -631,15 +1039,21 @@ export async function activateWorkspaceInstructionPolicyRevision(
|
|
|
631
1039
|
export async function rollbackWorkspaceInstructionPolicyRevision(
|
|
632
1040
|
db: Database,
|
|
633
1041
|
input: {
|
|
1042
|
+
operationId?: string;
|
|
634
1043
|
accountId: string;
|
|
635
1044
|
workspaceId: string;
|
|
636
1045
|
targetRevisionId: string;
|
|
637
1046
|
expectedCurrentRevisionId: string;
|
|
1047
|
+
expectedActivationVersion?: number;
|
|
638
1048
|
actorSubjectId: string;
|
|
639
1049
|
reason: string;
|
|
640
1050
|
},
|
|
641
1051
|
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
642
|
-
return await changeActiveRevision(db, {
|
|
1052
|
+
return await changeActiveRevision(db, {
|
|
1053
|
+
...input,
|
|
1054
|
+
operationId: input.operationId ?? randomUUID(),
|
|
1055
|
+
type: "rollback",
|
|
1056
|
+
});
|
|
643
1057
|
}
|
|
644
1058
|
|
|
645
1059
|
/**
|