@opengeni/db 0.23.0 → 0.27.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-EX7CDSGH.js} +196 -2
- package/dist/chunk-EX7CDSGH.js.map +1 -0
- package/dist/{chunk-L6ADMZHE.js → chunk-IHPCI4GV.js} +5 -1
- package/dist/chunk-IHPCI4GV.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +1457 -338
- 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 +3 -3
- package/dist/schema.d.ts +262 -4
- package/dist/schema.js +5 -1
- package/dist/session-realtime-mirror.d.ts +22 -1
- package/dist/workspace-instruction-policies-schema.d.ts +449 -0
- package/dist/workspace-instruction-policies.d.ts +88 -8
- 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/drizzle/0169_workspace_instruction_policy_onboarding_proposals.sql +202 -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 +4 -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 +116 -0
- package/src/workspace-instruction-policies.ts +819 -25
- 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,
|
|
@@ -14,19 +15,23 @@ import type {
|
|
|
14
15
|
WorkspaceInstructionPolicyKind,
|
|
15
16
|
WorkspaceInstructionPolicyListQuery,
|
|
16
17
|
WorkspaceInstructionPolicyListResponse,
|
|
18
|
+
WorkspaceInstructionPolicyOnboardingProposal,
|
|
19
|
+
WorkspaceInstructionPolicyOnboardingProposalListQuery,
|
|
20
|
+
WorkspaceInstructionPolicyOnboardingProposalListResponse,
|
|
17
21
|
WorkspaceInstructionPolicyProvenanceSource,
|
|
18
22
|
WorkspaceInstructionPolicyRevision,
|
|
19
23
|
WorkspaceInstructionPolicyScope,
|
|
20
24
|
WorkspaceInstructionPolicySnapshotEntry,
|
|
21
25
|
WorkspaceInstructionPolicyTarget,
|
|
22
26
|
} from "@opengeni/contracts";
|
|
23
|
-
import { and, asc, desc, eq, inArray, isNull, lt, sql, type SQL } from "drizzle-orm";
|
|
27
|
+
import { and, asc, desc, eq, inArray, isNull, lt, or, sql, type SQL } from "drizzle-orm";
|
|
24
28
|
import type { Database } from "./index";
|
|
25
|
-
import { withRlsContext, withWorkspaceRls } from "./index";
|
|
29
|
+
import { withRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./index";
|
|
26
30
|
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
27
31
|
import * as schema from "./schema";
|
|
28
32
|
|
|
29
|
-
type
|
|
33
|
+
type DraftRequestInput = WorkspaceInstructionPolicyTarget & {
|
|
34
|
+
operationId: string;
|
|
30
35
|
accountId: string;
|
|
31
36
|
workspaceId: string;
|
|
32
37
|
content: string;
|
|
@@ -36,10 +41,52 @@ type DraftInput = WorkspaceInstructionPolicyTarget & {
|
|
|
36
41
|
createdBySubjectId: string;
|
|
37
42
|
};
|
|
38
43
|
|
|
39
|
-
type
|
|
44
|
+
type DraftInput = DraftRequestInput & {
|
|
45
|
+
requestFingerprint: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type CallerDraftInput = Omit<DraftRequestInput, "operationId" | "provenanceSource"> & {
|
|
49
|
+
operationId?: string;
|
|
40
50
|
provenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource;
|
|
41
51
|
};
|
|
42
52
|
|
|
53
|
+
type ImportLegacyRequestInput = {
|
|
54
|
+
operationId: string;
|
|
55
|
+
accountId: string;
|
|
56
|
+
workspaceId: string;
|
|
57
|
+
createdBySubjectId: string;
|
|
58
|
+
supersedesRevisionId: string | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ActiveRevisionInput = {
|
|
62
|
+
operationId: string;
|
|
63
|
+
accountId: string;
|
|
64
|
+
workspaceId: string;
|
|
65
|
+
targetRevisionId: string;
|
|
66
|
+
expectedCurrentRevisionId: string | null;
|
|
67
|
+
expectedActivationVersion?: number;
|
|
68
|
+
actorSubjectId: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
type: WorkspaceInstructionPolicyActivationType;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type OnboardingProposalRequestInput = WorkspaceInstructionPolicyTarget & {
|
|
74
|
+
operationId: string;
|
|
75
|
+
accountId: string;
|
|
76
|
+
workspaceId: string;
|
|
77
|
+
content: string;
|
|
78
|
+
sourceId: string;
|
|
79
|
+
sourceVersion: string;
|
|
80
|
+
confidenceBps: number;
|
|
81
|
+
expectedCurrentRevisionId: string | null;
|
|
82
|
+
expectedActivationVersion: number;
|
|
83
|
+
createdBySubjectId: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type OnboardingProposalInput = OnboardingProposalRequestInput & {
|
|
87
|
+
requestFingerprint: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
43
90
|
export class WorkspaceInstructionPolicyConflictError extends Error {
|
|
44
91
|
readonly name = "WorkspaceInstructionPolicyConflictError";
|
|
45
92
|
readonly code = "WORKSPACE_INSTRUCTION_POLICY_CONFLICT";
|
|
@@ -49,6 +96,15 @@ export class WorkspaceInstructionPolicyConflictError extends Error {
|
|
|
49
96
|
}
|
|
50
97
|
}
|
|
51
98
|
|
|
99
|
+
export class WorkspaceInstructionPolicyOperationReuseError extends Error {
|
|
100
|
+
readonly name = "WorkspaceInstructionPolicyOperationReuseError";
|
|
101
|
+
readonly code = "WORKSPACE_INSTRUCTION_POLICY_OPERATION_REUSED";
|
|
102
|
+
|
|
103
|
+
constructor() {
|
|
104
|
+
super("The workspace instruction-policy operation id was already used for another request");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
52
108
|
export class WorkspaceInstructionPolicyNotFoundError extends Error {
|
|
53
109
|
readonly name = "WorkspaceInstructionPolicyNotFoundError";
|
|
54
110
|
|
|
@@ -69,6 +125,43 @@ export class WorkspaceInstructionPolicyLegacyUnavailableError extends Error {
|
|
|
69
125
|
}
|
|
70
126
|
}
|
|
71
127
|
|
|
128
|
+
export class WorkspaceInstructionPolicyOnboardingProposalContentError extends Error {
|
|
129
|
+
readonly name = "WorkspaceInstructionPolicyOnboardingProposalContentError";
|
|
130
|
+
|
|
131
|
+
constructor(
|
|
132
|
+
readonly code:
|
|
133
|
+
| "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY"
|
|
134
|
+
| "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_OVERSIZED",
|
|
135
|
+
) {
|
|
136
|
+
super(
|
|
137
|
+
code === "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY"
|
|
138
|
+
? "An onboarding proposal must contain non-blank instruction-policy content"
|
|
139
|
+
: `An onboarding proposal must not exceed ${WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class WorkspaceInstructionPolicyOnboardingProposalStaleError extends Error {
|
|
145
|
+
readonly name = "WorkspaceInstructionPolicyOnboardingProposalStaleError";
|
|
146
|
+
readonly code = "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_STALE";
|
|
147
|
+
|
|
148
|
+
constructor(readonly currentHead: WorkspaceInstructionPolicyHead | null) {
|
|
149
|
+
super("The onboarding proposal was prepared against a stale active policy baseline");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class WorkspaceInstructionPolicyOnboardingProposalConflictError extends Error {
|
|
154
|
+
readonly name = "WorkspaceInstructionPolicyOnboardingProposalConflictError";
|
|
155
|
+
readonly code = "WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_CONFLICT";
|
|
156
|
+
|
|
157
|
+
constructor(
|
|
158
|
+
readonly existingProposalId: string,
|
|
159
|
+
readonly existingDraftRevisionId: string,
|
|
160
|
+
) {
|
|
161
|
+
super("This onboarding source version already proposed a draft for the policy target");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
72
165
|
export class WorkspaceInstructionPolicySnapshotAuthorityError extends Error {
|
|
73
166
|
readonly name = "WorkspaceInstructionPolicySnapshotAuthorityError";
|
|
74
167
|
}
|
|
@@ -82,21 +175,258 @@ export type WorkspaceInstructionPolicyAttemptClaims = {
|
|
|
82
175
|
executionGeneration: number;
|
|
83
176
|
};
|
|
84
177
|
|
|
178
|
+
export type WorkspaceStateAcceptedAttemptGovernance = {
|
|
179
|
+
attemptId: string;
|
|
180
|
+
executionGeneration: number;
|
|
181
|
+
acceptedAt: string;
|
|
182
|
+
policySnapshot: WorkspaceInstructionPolicySnapshot | null;
|
|
183
|
+
preferenceSnapshot: {
|
|
184
|
+
id: string;
|
|
185
|
+
descriptorHash: string;
|
|
186
|
+
descriptors: Array<{
|
|
187
|
+
id: string;
|
|
188
|
+
revisionId: string;
|
|
189
|
+
contentHash: string;
|
|
190
|
+
activeVersion: number;
|
|
191
|
+
scope: "organization" | "workspace" | "user";
|
|
192
|
+
}>;
|
|
193
|
+
truncated: boolean;
|
|
194
|
+
createdAt: string;
|
|
195
|
+
} | null;
|
|
196
|
+
};
|
|
197
|
+
|
|
85
198
|
function contentHash(content: string): string {
|
|
86
199
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
87
200
|
}
|
|
88
201
|
|
|
202
|
+
type OperationFingerprintField = readonly [
|
|
203
|
+
name: string,
|
|
204
|
+
present: boolean,
|
|
205
|
+
value: string | number | null,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
function hasOwnField(value: object, field: PropertyKey): boolean {
|
|
209
|
+
return Object.prototype.hasOwnProperty.call(value, field);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function operationRequestFingerprint(
|
|
213
|
+
operation: string,
|
|
214
|
+
fields: readonly OperationFingerprintField[],
|
|
215
|
+
): string {
|
|
216
|
+
const canonicalRequest = JSON.stringify([
|
|
217
|
+
"workspace_instruction_policy_operation",
|
|
218
|
+
1,
|
|
219
|
+
operation,
|
|
220
|
+
fields,
|
|
221
|
+
]);
|
|
222
|
+
return createHash("sha256").update(canonicalRequest, "utf8").digest("hex");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function draftRequestFingerprint(input: DraftRequestInput): string {
|
|
226
|
+
return operationRequestFingerprint("create_draft", [
|
|
227
|
+
["accountId", true, input.accountId],
|
|
228
|
+
["workspaceId", true, input.workspaceId],
|
|
229
|
+
["kind", true, input.kind],
|
|
230
|
+
["scope", true, input.scope],
|
|
231
|
+
["roleKey", true, input.roleKey],
|
|
232
|
+
["content", true, input.content],
|
|
233
|
+
["provenanceSource", true, input.provenanceSource],
|
|
234
|
+
["provenanceSourceId", true, input.provenanceSourceId],
|
|
235
|
+
["supersedesRevisionId", true, input.supersedesRevisionId],
|
|
236
|
+
["createdBySubjectId", true, input.createdBySubjectId],
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function importLegacyRequestFingerprint(input: ImportLegacyRequestInput): string {
|
|
241
|
+
return operationRequestFingerprint("import_legacy", [
|
|
242
|
+
["accountId", true, input.accountId],
|
|
243
|
+
["workspaceId", true, input.workspaceId],
|
|
244
|
+
["createdBySubjectId", true, input.createdBySubjectId],
|
|
245
|
+
["supersedesRevisionId", true, input.supersedesRevisionId],
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function activeRevisionRequestFingerprint(input: ActiveRevisionInput): string {
|
|
250
|
+
const expectedCurrentRevisionIdPresent = hasOwnField(input, "expectedCurrentRevisionId");
|
|
251
|
+
const expectedActivationVersionPresent = hasOwnField(input, "expectedActivationVersion");
|
|
252
|
+
return operationRequestFingerprint(`change_active_revision:${input.type}`, [
|
|
253
|
+
["accountId", true, input.accountId],
|
|
254
|
+
["workspaceId", true, input.workspaceId],
|
|
255
|
+
["targetRevisionId", true, input.targetRevisionId],
|
|
256
|
+
[
|
|
257
|
+
"expectedCurrentRevisionId",
|
|
258
|
+
expectedCurrentRevisionIdPresent,
|
|
259
|
+
input.expectedCurrentRevisionId ?? null,
|
|
260
|
+
],
|
|
261
|
+
[
|
|
262
|
+
"expectedActivationVersion",
|
|
263
|
+
expectedActivationVersionPresent,
|
|
264
|
+
input.expectedActivationVersion ?? null,
|
|
265
|
+
],
|
|
266
|
+
["actorSubjectId", true, input.actorSubjectId],
|
|
267
|
+
["reason", true, input.reason],
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function onboardingProposalRequestFingerprint(input: OnboardingProposalRequestInput): string {
|
|
272
|
+
return operationRequestFingerprint("create_onboarding_proposal", [
|
|
273
|
+
["accountId", true, input.accountId],
|
|
274
|
+
["workspaceId", true, input.workspaceId],
|
|
275
|
+
["kind", true, input.kind],
|
|
276
|
+
["scope", true, input.scope],
|
|
277
|
+
["roleKey", true, input.roleKey],
|
|
278
|
+
["content", true, input.content],
|
|
279
|
+
["sourceId", true, input.sourceId],
|
|
280
|
+
["sourceVersion", true, input.sourceVersion],
|
|
281
|
+
["confidenceBps", true, input.confidenceBps],
|
|
282
|
+
["expectedCurrentRevisionId", true, input.expectedCurrentRevisionId],
|
|
283
|
+
["expectedActivationVersion", true, input.expectedActivationVersion],
|
|
284
|
+
["createdBySubjectId", true, input.createdBySubjectId],
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
|
|
89
288
|
function iso(value: Date | string): string {
|
|
90
289
|
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
91
290
|
}
|
|
92
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Inspect one immutable accepted attempt only when the authenticated caller is
|
|
294
|
+
* exactly its frozen initiating human. The null result deliberately conflates
|
|
295
|
+
* absence, cross-account/workspace lookup, and another subject's attempt.
|
|
296
|
+
*/
|
|
297
|
+
export async function getWorkspaceStateAcceptedAttemptGovernance(
|
|
298
|
+
db: Database,
|
|
299
|
+
input: { accountId: string; workspaceId: string; subjectId: string; attemptId: string },
|
|
300
|
+
): Promise<WorkspaceStateAcceptedAttemptGovernance | null> {
|
|
301
|
+
return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
|
|
302
|
+
const initiatingSubject = sql<string>`coalesce(
|
|
303
|
+
${schema.sessionTurns.initiatingHumanSubjectId},
|
|
304
|
+
case when ${schema.sessionTurns.initiatorKind} = 'subject'
|
|
305
|
+
then ${schema.sessionTurns.initiatorSubjectId}
|
|
306
|
+
end
|
|
307
|
+
)`;
|
|
308
|
+
const [attempt] = await scopedDb
|
|
309
|
+
.select({
|
|
310
|
+
attemptId: schema.sessionTurnAttempts.id,
|
|
311
|
+
executionGeneration: schema.sessionTurnAttempts.executionGeneration,
|
|
312
|
+
acceptedAt: schema.sessionTurns.createdAt,
|
|
313
|
+
})
|
|
314
|
+
.from(schema.sessionTurnAttempts)
|
|
315
|
+
.innerJoin(
|
|
316
|
+
schema.sessionTurns,
|
|
317
|
+
and(
|
|
318
|
+
eq(schema.sessionTurns.accountId, schema.sessionTurnAttempts.accountId),
|
|
319
|
+
eq(schema.sessionTurns.workspaceId, schema.sessionTurnAttempts.workspaceId),
|
|
320
|
+
eq(schema.sessionTurns.sessionId, schema.sessionTurnAttempts.sessionId),
|
|
321
|
+
eq(schema.sessionTurns.id, schema.sessionTurnAttempts.turnId),
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
.where(
|
|
325
|
+
and(
|
|
326
|
+
eq(schema.sessionTurnAttempts.accountId, input.accountId),
|
|
327
|
+
eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
328
|
+
eq(schema.sessionTurnAttempts.id, input.attemptId),
|
|
329
|
+
eq(initiatingSubject, input.subjectId),
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
.limit(1);
|
|
333
|
+
if (!attempt) return null;
|
|
334
|
+
|
|
335
|
+
const [policyRows, preferenceRows] = await Promise.all([
|
|
336
|
+
scopedDb
|
|
337
|
+
.select()
|
|
338
|
+
.from(schema.workspaceInstructionPolicySnapshots)
|
|
339
|
+
.where(
|
|
340
|
+
and(
|
|
341
|
+
eq(schema.workspaceInstructionPolicySnapshots.accountId, input.accountId),
|
|
342
|
+
eq(schema.workspaceInstructionPolicySnapshots.workspaceId, input.workspaceId),
|
|
343
|
+
eq(schema.workspaceInstructionPolicySnapshots.attemptId, input.attemptId),
|
|
344
|
+
eq(
|
|
345
|
+
schema.workspaceInstructionPolicySnapshots.executionGeneration,
|
|
346
|
+
attempt.executionGeneration,
|
|
347
|
+
),
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
.limit(1),
|
|
351
|
+
scopedDb
|
|
352
|
+
.select()
|
|
353
|
+
.from(schema.preferenceRegistrySnapshots)
|
|
354
|
+
.where(
|
|
355
|
+
and(
|
|
356
|
+
eq(schema.preferenceRegistrySnapshots.accountId, input.accountId),
|
|
357
|
+
eq(schema.preferenceRegistrySnapshots.workspaceId, input.workspaceId),
|
|
358
|
+
eq(schema.preferenceRegistrySnapshots.attemptId, input.attemptId),
|
|
359
|
+
eq(schema.preferenceRegistrySnapshots.executionGeneration, attempt.executionGeneration),
|
|
360
|
+
eq(schema.preferenceRegistrySnapshots.initiatingHumanSubjectId, input.subjectId),
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
.limit(1),
|
|
364
|
+
]);
|
|
365
|
+
const policyRow = policyRows[0] ?? null;
|
|
366
|
+
const preferenceRow = preferenceRows[0] ?? null;
|
|
367
|
+
return {
|
|
368
|
+
attemptId: attempt.attemptId,
|
|
369
|
+
executionGeneration: attempt.executionGeneration,
|
|
370
|
+
acceptedAt: iso(attempt.acceptedAt),
|
|
371
|
+
policySnapshot: policyRow
|
|
372
|
+
? WorkspaceInstructionPolicySnapshot.parse({
|
|
373
|
+
id: policyRow.id,
|
|
374
|
+
workspaceId: policyRow.workspaceId,
|
|
375
|
+
sessionId: policyRow.sessionId,
|
|
376
|
+
turnId: policyRow.turnId,
|
|
377
|
+
attemptId: policyRow.attemptId,
|
|
378
|
+
executionGeneration: policyRow.executionGeneration,
|
|
379
|
+
policyRole: policyRow.policyRole,
|
|
380
|
+
roleSource: policyRow.roleSource,
|
|
381
|
+
entries: policyRow.entries,
|
|
382
|
+
entryHash: policyRow.entryHash,
|
|
383
|
+
createdAt: iso(policyRow.createdAt),
|
|
384
|
+
})
|
|
385
|
+
: null,
|
|
386
|
+
preferenceSnapshot: preferenceRow
|
|
387
|
+
? (() => {
|
|
388
|
+
const snapshot = PreferenceRegistrySnapshot.parse({
|
|
389
|
+
id: preferenceRow.id,
|
|
390
|
+
workspaceId: preferenceRow.workspaceId,
|
|
391
|
+
sessionId: preferenceRow.sessionId,
|
|
392
|
+
turnId: preferenceRow.turnId,
|
|
393
|
+
attemptId: preferenceRow.attemptId,
|
|
394
|
+
executionGeneration: preferenceRow.executionGeneration,
|
|
395
|
+
initiatingHumanSubjectId: preferenceRow.initiatingHumanSubjectId,
|
|
396
|
+
descriptorHash: preferenceRow.descriptorHash,
|
|
397
|
+
descriptors: preferenceRow.descriptors,
|
|
398
|
+
truncated: preferenceRow.truncated,
|
|
399
|
+
createdAt: iso(preferenceRow.createdAt),
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
id: snapshot.id,
|
|
403
|
+
descriptorHash: snapshot.descriptorHash,
|
|
404
|
+
descriptors: snapshot.descriptors.map((descriptor) => ({
|
|
405
|
+
id: descriptor.id,
|
|
406
|
+
revisionId: descriptor.revisionId,
|
|
407
|
+
contentHash: descriptor.contentHash,
|
|
408
|
+
activeVersion: descriptor.activeVersion,
|
|
409
|
+
scope: descriptor.scope,
|
|
410
|
+
})),
|
|
411
|
+
truncated: snapshot.truncated,
|
|
412
|
+
createdAt: snapshot.createdAt,
|
|
413
|
+
};
|
|
414
|
+
})()
|
|
415
|
+
: null,
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
93
420
|
type RevisionRow = typeof schema.workspaceInstructionPolicyRevisions.$inferSelect;
|
|
94
421
|
type HeadRow = typeof schema.workspaceInstructionPolicyHeads.$inferSelect;
|
|
95
422
|
type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferSelect;
|
|
423
|
+
type OnboardingProposalRow =
|
|
424
|
+
typeof schema.workspaceInstructionPolicyOnboardingProposals.$inferSelect;
|
|
96
425
|
|
|
97
426
|
function revisionFromRow(row: RevisionRow): WorkspaceInstructionPolicyRevision {
|
|
98
427
|
return {
|
|
99
428
|
id: row.id,
|
|
429
|
+
operationId: row.operationId ?? row.id,
|
|
100
430
|
accountId: row.accountId,
|
|
101
431
|
workspaceId: row.workspaceId,
|
|
102
432
|
revision: row.revision,
|
|
@@ -132,6 +462,7 @@ function headFromRow(row: HeadRow): WorkspaceInstructionPolicyHead {
|
|
|
132
462
|
function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent {
|
|
133
463
|
return {
|
|
134
464
|
id: row.id,
|
|
465
|
+
operationId: row.operationId ?? row.id,
|
|
135
466
|
accountId: row.accountId,
|
|
136
467
|
workspaceId: row.workspaceId,
|
|
137
468
|
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
@@ -158,6 +489,58 @@ function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent
|
|
|
158
489
|
};
|
|
159
490
|
}
|
|
160
491
|
|
|
492
|
+
function onboardingProposalFromRow(
|
|
493
|
+
row: OnboardingProposalRow,
|
|
494
|
+
draftRow: RevisionRow,
|
|
495
|
+
): WorkspaceInstructionPolicyOnboardingProposal {
|
|
496
|
+
return {
|
|
497
|
+
id: row.id,
|
|
498
|
+
operationId: row.operationId,
|
|
499
|
+
accountId: row.accountId,
|
|
500
|
+
workspaceId: row.workspaceId,
|
|
501
|
+
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
502
|
+
scope: row.scope as WorkspaceInstructionPolicyScope,
|
|
503
|
+
roleKey: row.roleKey,
|
|
504
|
+
source: {
|
|
505
|
+
id: row.sourceId,
|
|
506
|
+
version: row.sourceVersion,
|
|
507
|
+
confidenceBps: row.confidenceBps,
|
|
508
|
+
},
|
|
509
|
+
baseline:
|
|
510
|
+
row.baselineRevisionId === null
|
|
511
|
+
? null
|
|
512
|
+
: {
|
|
513
|
+
workspaceId: row.workspaceId,
|
|
514
|
+
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
515
|
+
scope: row.scope as WorkspaceInstructionPolicyScope,
|
|
516
|
+
roleKey: row.roleKey,
|
|
517
|
+
revisionId: row.baselineRevisionId,
|
|
518
|
+
revision: row.baselineRevision!,
|
|
519
|
+
contentHash: row.baselineContentHash!,
|
|
520
|
+
activationVersion: row.baselineActivationVersion,
|
|
521
|
+
activatedAt: iso(row.baselineActivatedAt!),
|
|
522
|
+
},
|
|
523
|
+
draft: revisionFromRow(draftRow),
|
|
524
|
+
status: "proposed",
|
|
525
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
526
|
+
createdAt: iso(row.createdAt),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function headFromEventRow(row: EventRow): WorkspaceInstructionPolicyHead {
|
|
531
|
+
return {
|
|
532
|
+
workspaceId: row.workspaceId,
|
|
533
|
+
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
534
|
+
scope: row.scope as WorkspaceInstructionPolicyScope,
|
|
535
|
+
roleKey: row.roleKey,
|
|
536
|
+
revisionId: row.newRevisionId,
|
|
537
|
+
revision: row.newRevision,
|
|
538
|
+
contentHash: row.newContentHash,
|
|
539
|
+
activationVersion: row.activationVersion,
|
|
540
|
+
activatedAt: iso(row.createdAt),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
161
544
|
function headTargetConditions(
|
|
162
545
|
workspaceId: string,
|
|
163
546
|
target: WorkspaceInstructionPolicyTarget,
|
|
@@ -225,6 +608,109 @@ async function getRevisionInTransaction(
|
|
|
225
608
|
return row ?? null;
|
|
226
609
|
}
|
|
227
610
|
|
|
611
|
+
async function getRevisionByOperationInTransaction(
|
|
612
|
+
db: Database,
|
|
613
|
+
workspaceId: string,
|
|
614
|
+
operationId: string,
|
|
615
|
+
): Promise<RevisionRow | null> {
|
|
616
|
+
const [row] = await db
|
|
617
|
+
.select()
|
|
618
|
+
.from(schema.workspaceInstructionPolicyRevisions)
|
|
619
|
+
.where(
|
|
620
|
+
and(
|
|
621
|
+
eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
|
|
622
|
+
or(
|
|
623
|
+
eq(schema.workspaceInstructionPolicyRevisions.operationId, operationId),
|
|
624
|
+
and(
|
|
625
|
+
isNull(schema.workspaceInstructionPolicyRevisions.operationId),
|
|
626
|
+
eq(schema.workspaceInstructionPolicyRevisions.id, operationId),
|
|
627
|
+
),
|
|
628
|
+
),
|
|
629
|
+
),
|
|
630
|
+
)
|
|
631
|
+
.limit(1);
|
|
632
|
+
return row ?? null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function getEventByOperationInTransaction(
|
|
636
|
+
db: Database,
|
|
637
|
+
workspaceId: string,
|
|
638
|
+
operationId: string,
|
|
639
|
+
): Promise<EventRow | null> {
|
|
640
|
+
const [row] = await db
|
|
641
|
+
.select()
|
|
642
|
+
.from(schema.workspaceInstructionPolicyActivationEvents)
|
|
643
|
+
.where(
|
|
644
|
+
and(
|
|
645
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId),
|
|
646
|
+
or(
|
|
647
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.operationId, operationId),
|
|
648
|
+
and(
|
|
649
|
+
isNull(schema.workspaceInstructionPolicyActivationEvents.operationId),
|
|
650
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.id, operationId),
|
|
651
|
+
),
|
|
652
|
+
),
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
.limit(1);
|
|
656
|
+
return row ?? null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function getOnboardingProposalByOperationInTransaction(
|
|
660
|
+
db: Database,
|
|
661
|
+
workspaceId: string,
|
|
662
|
+
operationId: string,
|
|
663
|
+
): Promise<OnboardingProposalRow | null> {
|
|
664
|
+
const [row] = await db
|
|
665
|
+
.select()
|
|
666
|
+
.from(schema.workspaceInstructionPolicyOnboardingProposals)
|
|
667
|
+
.where(
|
|
668
|
+
and(
|
|
669
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, workspaceId),
|
|
670
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.operationId, operationId),
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
.limit(1);
|
|
674
|
+
return row ?? null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function getOnboardingProposalBySourceVersionInTransaction(
|
|
678
|
+
db: Database,
|
|
679
|
+
input: Pick<
|
|
680
|
+
OnboardingProposalRequestInput,
|
|
681
|
+
"workspaceId" | "kind" | "scope" | "roleKey" | "sourceId" | "sourceVersion"
|
|
682
|
+
>,
|
|
683
|
+
): Promise<OnboardingProposalRow | null> {
|
|
684
|
+
const [row] = await db
|
|
685
|
+
.select()
|
|
686
|
+
.from(schema.workspaceInstructionPolicyOnboardingProposals)
|
|
687
|
+
.where(
|
|
688
|
+
and(
|
|
689
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, input.workspaceId),
|
|
690
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.kind, input.kind),
|
|
691
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.scope, input.scope),
|
|
692
|
+
input.roleKey === null
|
|
693
|
+
? isNull(schema.workspaceInstructionPolicyOnboardingProposals.roleKey)
|
|
694
|
+
: eq(schema.workspaceInstructionPolicyOnboardingProposals.roleKey, input.roleKey),
|
|
695
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.sourceId, input.sourceId),
|
|
696
|
+
eq(schema.workspaceInstructionPolicyOnboardingProposals.sourceVersion, input.sourceVersion),
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
.limit(1);
|
|
700
|
+
return row ?? null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function resolveOnboardingProposalInTransaction(
|
|
704
|
+
db: Database,
|
|
705
|
+
row: OnboardingProposalRow,
|
|
706
|
+
): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
|
|
707
|
+
const draftRow = await getRevisionInTransaction(db, row.workspaceId, row.draftRevisionId);
|
|
708
|
+
if (!draftRow) {
|
|
709
|
+
throw new Error("Workspace instruction-policy onboarding proposal lost its draft revision");
|
|
710
|
+
}
|
|
711
|
+
return onboardingProposalFromRow(row, draftRow);
|
|
712
|
+
}
|
|
713
|
+
|
|
228
714
|
async function getHeadInTransaction(
|
|
229
715
|
db: Database,
|
|
230
716
|
workspaceId: string,
|
|
@@ -246,10 +732,41 @@ function sameTarget(
|
|
|
246
732
|
return left.kind === right.kind && left.scope === right.scope && left.roleKey === right.roleKey;
|
|
247
733
|
}
|
|
248
734
|
|
|
735
|
+
function draftReceiptMatches(row: RevisionRow, input: DraftInput): boolean {
|
|
736
|
+
return (
|
|
737
|
+
row.createdBySubjectId === input.createdBySubjectId &&
|
|
738
|
+
row.kind === input.kind &&
|
|
739
|
+
row.scope === input.scope &&
|
|
740
|
+
row.roleKey === input.roleKey &&
|
|
741
|
+
row.content === input.content &&
|
|
742
|
+
row.provenanceSource === input.provenanceSource &&
|
|
743
|
+
row.provenanceSourceId === input.provenanceSourceId &&
|
|
744
|
+
row.supersedesRevisionId === input.supersedesRevisionId
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
249
748
|
async function createDraftInTransaction(
|
|
250
749
|
db: Database,
|
|
251
750
|
input: DraftInput,
|
|
252
751
|
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
752
|
+
if (await getEventByOperationInTransaction(db, input.workspaceId, input.operationId)) {
|
|
753
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
754
|
+
}
|
|
755
|
+
const existing = await getRevisionByOperationInTransaction(
|
|
756
|
+
db,
|
|
757
|
+
input.workspaceId,
|
|
758
|
+
input.operationId,
|
|
759
|
+
);
|
|
760
|
+
if (existing) {
|
|
761
|
+
if (
|
|
762
|
+
existing.requestFingerprint === null
|
|
763
|
+
? !draftReceiptMatches(existing, input)
|
|
764
|
+
: existing.requestFingerprint !== input.requestFingerprint
|
|
765
|
+
) {
|
|
766
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
767
|
+
}
|
|
768
|
+
return revisionFromRow(existing);
|
|
769
|
+
}
|
|
253
770
|
if (input.supersedesRevisionId !== null) {
|
|
254
771
|
const superseded = await getRevisionInTransaction(
|
|
255
772
|
db,
|
|
@@ -265,6 +782,8 @@ async function createDraftInTransaction(
|
|
|
265
782
|
const [created] = await db
|
|
266
783
|
.insert(schema.workspaceInstructionPolicyRevisions)
|
|
267
784
|
.values({
|
|
785
|
+
operationId: input.operationId,
|
|
786
|
+
requestFingerprint: input.requestFingerprint,
|
|
268
787
|
accountId: input.accountId,
|
|
269
788
|
workspaceId: input.workspaceId,
|
|
270
789
|
kind: input.kind,
|
|
@@ -277,8 +796,19 @@ async function createDraftInTransaction(
|
|
|
277
796
|
supersedesRevisionId: input.supersedesRevisionId,
|
|
278
797
|
createdBySubjectId: input.createdBySubjectId,
|
|
279
798
|
})
|
|
799
|
+
.onConflictDoNothing()
|
|
280
800
|
.returning();
|
|
281
|
-
if (!created)
|
|
801
|
+
if (!created) {
|
|
802
|
+
const concurrent = await getRevisionByOperationInTransaction(
|
|
803
|
+
db,
|
|
804
|
+
input.workspaceId,
|
|
805
|
+
input.operationId,
|
|
806
|
+
);
|
|
807
|
+
if (!concurrent || concurrent.requestFingerprint !== input.requestFingerprint) {
|
|
808
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
809
|
+
}
|
|
810
|
+
return revisionFromRow(concurrent);
|
|
811
|
+
}
|
|
282
812
|
return revisionFromRow(created);
|
|
283
813
|
}
|
|
284
814
|
|
|
@@ -286,12 +816,14 @@ export async function createWorkspaceInstructionPolicyDraft(
|
|
|
286
816
|
db: Database,
|
|
287
817
|
input: CallerDraftInput,
|
|
288
818
|
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
819
|
+
const request = { ...input, operationId: input.operationId ?? randomUUID() };
|
|
820
|
+
const normalized = { ...request, requestFingerprint: draftRequestFingerprint(request) };
|
|
289
821
|
return await withRlsContext(
|
|
290
822
|
db,
|
|
291
823
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
292
824
|
async (scopedDb) => {
|
|
293
825
|
await lockWorkspace(scopedDb, input);
|
|
294
|
-
return await createDraftInTransaction(scopedDb,
|
|
826
|
+
return await createDraftInTransaction(scopedDb, normalized);
|
|
295
827
|
},
|
|
296
828
|
);
|
|
297
829
|
}
|
|
@@ -299,18 +831,44 @@ export async function createWorkspaceInstructionPolicyDraft(
|
|
|
299
831
|
/** Import only the stored legacy override; never materialize a deployment default or activate it. */
|
|
300
832
|
export async function importLegacyWorkspaceInstructionPolicyDraft(
|
|
301
833
|
db: Database,
|
|
302
|
-
input: {
|
|
303
|
-
accountId: string;
|
|
304
|
-
workspaceId: string;
|
|
305
|
-
createdBySubjectId: string;
|
|
306
|
-
supersedesRevisionId: string | null;
|
|
307
|
-
},
|
|
834
|
+
input: Omit<ImportLegacyRequestInput, "operationId"> & { operationId?: string },
|
|
308
835
|
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
836
|
+
const request = { ...input, operationId: input.operationId ?? randomUUID() };
|
|
837
|
+
const normalized = {
|
|
838
|
+
...request,
|
|
839
|
+
requestFingerprint: importLegacyRequestFingerprint(request),
|
|
840
|
+
};
|
|
309
841
|
return await withRlsContext(
|
|
310
842
|
db,
|
|
311
843
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
312
844
|
async (scopedDb) => {
|
|
313
845
|
const workspace = await lockWorkspace(scopedDb, input);
|
|
846
|
+
if (
|
|
847
|
+
await getEventByOperationInTransaction(scopedDb, input.workspaceId, normalized.operationId)
|
|
848
|
+
) {
|
|
849
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
850
|
+
}
|
|
851
|
+
const existing = await getRevisionByOperationInTransaction(
|
|
852
|
+
scopedDb,
|
|
853
|
+
input.workspaceId,
|
|
854
|
+
normalized.operationId,
|
|
855
|
+
);
|
|
856
|
+
if (existing) {
|
|
857
|
+
if (
|
|
858
|
+
existing.requestFingerprint === null
|
|
859
|
+
? existing.createdBySubjectId !== input.createdBySubjectId ||
|
|
860
|
+
existing.kind !== "charter" ||
|
|
861
|
+
existing.scope !== "global" ||
|
|
862
|
+
existing.roleKey !== null ||
|
|
863
|
+
existing.provenanceSource !== "legacy_import" ||
|
|
864
|
+
existing.provenanceSourceId !== "workspaces.agent_instructions" ||
|
|
865
|
+
existing.supersedesRevisionId !== input.supersedesRevisionId
|
|
866
|
+
: existing.requestFingerprint !== normalized.requestFingerprint
|
|
867
|
+
) {
|
|
868
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
869
|
+
}
|
|
870
|
+
return revisionFromRow(existing);
|
|
871
|
+
}
|
|
314
872
|
if (workspace.agentInstructions === null) {
|
|
315
873
|
throw new WorkspaceInstructionPolicyLegacyUnavailableError();
|
|
316
874
|
}
|
|
@@ -323,7 +881,7 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
|
|
|
323
881
|
);
|
|
324
882
|
}
|
|
325
883
|
return await createDraftInTransaction(scopedDb, {
|
|
326
|
-
...
|
|
884
|
+
...normalized,
|
|
327
885
|
kind: "charter",
|
|
328
886
|
scope: "global",
|
|
329
887
|
roleKey: null,
|
|
@@ -335,6 +893,206 @@ export async function importLegacyWorkspaceInstructionPolicyDraft(
|
|
|
335
893
|
);
|
|
336
894
|
}
|
|
337
895
|
|
|
896
|
+
export async function createWorkspaceInstructionPolicyOnboardingProposal(
|
|
897
|
+
db: Database,
|
|
898
|
+
input: Omit<OnboardingProposalRequestInput, "operationId"> & { operationId?: string },
|
|
899
|
+
): Promise<WorkspaceInstructionPolicyOnboardingProposal> {
|
|
900
|
+
if (input.content.trim().length === 0) {
|
|
901
|
+
throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
|
|
902
|
+
"WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_EMPTY",
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (input.content.length > WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS) {
|
|
906
|
+
throw new WorkspaceInstructionPolicyOnboardingProposalContentError(
|
|
907
|
+
"WORKSPACE_INSTRUCTION_POLICY_ONBOARDING_PROPOSAL_OVERSIZED",
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const request: OnboardingProposalRequestInput = {
|
|
911
|
+
...input,
|
|
912
|
+
operationId: input.operationId ?? randomUUID(),
|
|
913
|
+
sourceId: input.sourceId.trim(),
|
|
914
|
+
sourceVersion: input.sourceVersion.trim(),
|
|
915
|
+
};
|
|
916
|
+
const normalized: OnboardingProposalInput = {
|
|
917
|
+
...request,
|
|
918
|
+
requestFingerprint: onboardingProposalRequestFingerprint(request),
|
|
919
|
+
};
|
|
920
|
+
return await withRlsContext(
|
|
921
|
+
db,
|
|
922
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
923
|
+
async (scopedDb) => {
|
|
924
|
+
await lockWorkspace(scopedDb, input);
|
|
925
|
+
const existing = await getOnboardingProposalByOperationInTransaction(
|
|
926
|
+
scopedDb,
|
|
927
|
+
input.workspaceId,
|
|
928
|
+
normalized.operationId,
|
|
929
|
+
);
|
|
930
|
+
if (existing) {
|
|
931
|
+
if (existing.requestFingerprint !== normalized.requestFingerprint) {
|
|
932
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
933
|
+
}
|
|
934
|
+
return await resolveOnboardingProposalInTransaction(scopedDb, existing);
|
|
935
|
+
}
|
|
936
|
+
if (
|
|
937
|
+
(await getEventByOperationInTransaction(
|
|
938
|
+
scopedDb,
|
|
939
|
+
input.workspaceId,
|
|
940
|
+
normalized.operationId,
|
|
941
|
+
)) ||
|
|
942
|
+
(await getRevisionByOperationInTransaction(
|
|
943
|
+
scopedDb,
|
|
944
|
+
input.workspaceId,
|
|
945
|
+
normalized.operationId,
|
|
946
|
+
))
|
|
947
|
+
) {
|
|
948
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const target: WorkspaceInstructionPolicyTarget = {
|
|
952
|
+
kind: normalized.kind,
|
|
953
|
+
scope: normalized.scope,
|
|
954
|
+
roleKey: normalized.roleKey,
|
|
955
|
+
};
|
|
956
|
+
const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, target);
|
|
957
|
+
const currentHead = currentRow ? headFromRow(currentRow) : null;
|
|
958
|
+
if (
|
|
959
|
+
(currentHead?.revisionId ?? null) !== normalized.expectedCurrentRevisionId ||
|
|
960
|
+
(currentHead?.activationVersion ?? 0) !== normalized.expectedActivationVersion
|
|
961
|
+
) {
|
|
962
|
+
throw new WorkspaceInstructionPolicyOnboardingProposalStaleError(currentHead);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const sourceConflict = await getOnboardingProposalBySourceVersionInTransaction(
|
|
966
|
+
scopedDb,
|
|
967
|
+
normalized,
|
|
968
|
+
);
|
|
969
|
+
if (sourceConflict) {
|
|
970
|
+
throw new WorkspaceInstructionPolicyOnboardingProposalConflictError(
|
|
971
|
+
sourceConflict.id,
|
|
972
|
+
sourceConflict.draftRevisionId,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const proposalId = randomUUID();
|
|
977
|
+
const draft = await createDraftInTransaction(scopedDb, {
|
|
978
|
+
operationId: normalized.operationId,
|
|
979
|
+
requestFingerprint: normalized.requestFingerprint,
|
|
980
|
+
accountId: normalized.accountId,
|
|
981
|
+
workspaceId: normalized.workspaceId,
|
|
982
|
+
kind: normalized.kind,
|
|
983
|
+
scope: normalized.scope,
|
|
984
|
+
roleKey: normalized.roleKey,
|
|
985
|
+
content: normalized.content,
|
|
986
|
+
provenanceSource: "onboarding",
|
|
987
|
+
provenanceSourceId: proposalId,
|
|
988
|
+
supersedesRevisionId: currentHead?.revisionId ?? null,
|
|
989
|
+
createdBySubjectId: normalized.createdBySubjectId,
|
|
990
|
+
});
|
|
991
|
+
const createdAt = new Date();
|
|
992
|
+
const [created] = await scopedDb
|
|
993
|
+
.insert(schema.workspaceInstructionPolicyOnboardingProposals)
|
|
994
|
+
.values({
|
|
995
|
+
id: proposalId,
|
|
996
|
+
operationId: normalized.operationId,
|
|
997
|
+
requestFingerprint: normalized.requestFingerprint,
|
|
998
|
+
accountId: normalized.accountId,
|
|
999
|
+
workspaceId: normalized.workspaceId,
|
|
1000
|
+
kind: normalized.kind,
|
|
1001
|
+
scope: normalized.scope,
|
|
1002
|
+
roleKey: normalized.roleKey,
|
|
1003
|
+
sourceId: normalized.sourceId,
|
|
1004
|
+
sourceVersion: normalized.sourceVersion,
|
|
1005
|
+
confidenceBps: normalized.confidenceBps,
|
|
1006
|
+
baselineRevisionId: currentHead?.revisionId ?? null,
|
|
1007
|
+
baselineRevision: currentHead?.revision ?? null,
|
|
1008
|
+
baselineContentHash: currentHead?.contentHash ?? null,
|
|
1009
|
+
baselineActivationVersion: currentHead?.activationVersion ?? 0,
|
|
1010
|
+
baselineActivatedAt: currentHead ? new Date(currentHead.activatedAt) : null,
|
|
1011
|
+
draftRevisionId: draft.id,
|
|
1012
|
+
draftRevision: draft.revision,
|
|
1013
|
+
draftContentHash: draft.contentHash,
|
|
1014
|
+
status: "proposed",
|
|
1015
|
+
createdBySubjectId: normalized.createdBySubjectId,
|
|
1016
|
+
createdAt,
|
|
1017
|
+
})
|
|
1018
|
+
.onConflictDoNothing()
|
|
1019
|
+
.returning();
|
|
1020
|
+
if (created) {
|
|
1021
|
+
const draftRow = await getRevisionInTransaction(scopedDb, input.workspaceId, draft.id);
|
|
1022
|
+
if (!draftRow) throw new Error("Onboarding proposal draft was not recorded");
|
|
1023
|
+
return onboardingProposalFromRow(created, draftRow);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const concurrentOperation = await getOnboardingProposalByOperationInTransaction(
|
|
1027
|
+
scopedDb,
|
|
1028
|
+
input.workspaceId,
|
|
1029
|
+
normalized.operationId,
|
|
1030
|
+
);
|
|
1031
|
+
if (concurrentOperation) {
|
|
1032
|
+
if (concurrentOperation.requestFingerprint !== normalized.requestFingerprint) {
|
|
1033
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
1034
|
+
}
|
|
1035
|
+
return await resolveOnboardingProposalInTransaction(scopedDb, concurrentOperation);
|
|
1036
|
+
}
|
|
1037
|
+
const concurrentSource = await getOnboardingProposalBySourceVersionInTransaction(
|
|
1038
|
+
scopedDb,
|
|
1039
|
+
normalized,
|
|
1040
|
+
);
|
|
1041
|
+
if (concurrentSource) {
|
|
1042
|
+
throw new WorkspaceInstructionPolicyOnboardingProposalConflictError(
|
|
1043
|
+
concurrentSource.id,
|
|
1044
|
+
concurrentSource.draftRevisionId,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
throw new Error("Workspace instruction-policy onboarding proposal was not created");
|
|
1048
|
+
},
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export async function listWorkspaceInstructionPolicyOnboardingProposals(
|
|
1053
|
+
db: Database,
|
|
1054
|
+
workspaceId: string,
|
|
1055
|
+
query: WorkspaceInstructionPolicyOnboardingProposalListQuery,
|
|
1056
|
+
): Promise<WorkspaceInstructionPolicyOnboardingProposalListResponse> {
|
|
1057
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
1058
|
+
const rows = await scopedDb
|
|
1059
|
+
.select()
|
|
1060
|
+
.from(schema.workspaceInstructionPolicyOnboardingProposals)
|
|
1061
|
+
.where(eq(schema.workspaceInstructionPolicyOnboardingProposals.workspaceId, workspaceId))
|
|
1062
|
+
.orderBy(
|
|
1063
|
+
desc(schema.workspaceInstructionPolicyOnboardingProposals.createdAt),
|
|
1064
|
+
desc(schema.workspaceInstructionPolicyOnboardingProposals.id),
|
|
1065
|
+
)
|
|
1066
|
+
.limit(query.limit + 1);
|
|
1067
|
+
const truncated = rows.length > query.limit;
|
|
1068
|
+
const page = rows.slice(0, query.limit);
|
|
1069
|
+
if (page.length === 0) return { proposals: [], truncated };
|
|
1070
|
+
const revisions = await scopedDb
|
|
1071
|
+
.select()
|
|
1072
|
+
.from(schema.workspaceInstructionPolicyRevisions)
|
|
1073
|
+
.where(
|
|
1074
|
+
and(
|
|
1075
|
+
eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
|
|
1076
|
+
inArray(
|
|
1077
|
+
schema.workspaceInstructionPolicyRevisions.id,
|
|
1078
|
+
page.map((proposal) => proposal.draftRevisionId),
|
|
1079
|
+
),
|
|
1080
|
+
),
|
|
1081
|
+
);
|
|
1082
|
+
const revisionsById = new Map(revisions.map((revision) => [revision.id, revision]));
|
|
1083
|
+
return {
|
|
1084
|
+
proposals: page.map((proposal) => {
|
|
1085
|
+
const draft = revisionsById.get(proposal.draftRevisionId);
|
|
1086
|
+
if (!draft) {
|
|
1087
|
+
throw new Error("Workspace instruction-policy onboarding proposal lost its draft");
|
|
1088
|
+
}
|
|
1089
|
+
return onboardingProposalFromRow(proposal, draft);
|
|
1090
|
+
}),
|
|
1091
|
+
truncated,
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
338
1096
|
function listFilterConditions(
|
|
339
1097
|
workspaceId: string,
|
|
340
1098
|
query: WorkspaceInstructionPolicyListQuery,
|
|
@@ -493,16 +1251,9 @@ export async function diffWorkspaceInstructionPolicyRevisions(
|
|
|
493
1251
|
|
|
494
1252
|
async function changeActiveRevision(
|
|
495
1253
|
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
|
-
},
|
|
1254
|
+
input: ActiveRevisionInput,
|
|
505
1255
|
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
1256
|
+
const requestFingerprint = activeRevisionRequestFingerprint(input);
|
|
506
1257
|
return await withRlsContext(
|
|
507
1258
|
db,
|
|
508
1259
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
@@ -510,6 +1261,34 @@ async function changeActiveRevision(
|
|
|
510
1261
|
// This row lock serializes both an absent-head first activation and later
|
|
511
1262
|
// updates, so every loser can return the authoritative typed conflict.
|
|
512
1263
|
await lockWorkspace(scopedDb, input);
|
|
1264
|
+
if (
|
|
1265
|
+
await getRevisionByOperationInTransaction(scopedDb, input.workspaceId, input.operationId)
|
|
1266
|
+
) {
|
|
1267
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
1268
|
+
}
|
|
1269
|
+
const existingEvent = await getEventByOperationInTransaction(
|
|
1270
|
+
scopedDb,
|
|
1271
|
+
input.workspaceId,
|
|
1272
|
+
input.operationId,
|
|
1273
|
+
);
|
|
1274
|
+
if (existingEvent) {
|
|
1275
|
+
const legacyRequestMatches =
|
|
1276
|
+
hasOwnField(input, "expectedCurrentRevisionId") &&
|
|
1277
|
+
!hasOwnField(input, "expectedActivationVersion") &&
|
|
1278
|
+
existingEvent.type === input.type &&
|
|
1279
|
+
existingEvent.newRevisionId === input.targetRevisionId &&
|
|
1280
|
+
existingEvent.oldRevisionId === input.expectedCurrentRevisionId &&
|
|
1281
|
+
existingEvent.actorSubjectId === input.actorSubjectId &&
|
|
1282
|
+
existingEvent.reason === input.reason;
|
|
1283
|
+
if (
|
|
1284
|
+
existingEvent.requestFingerprint === null
|
|
1285
|
+
? !legacyRequestMatches
|
|
1286
|
+
: existingEvent.requestFingerprint !== requestFingerprint
|
|
1287
|
+
) {
|
|
1288
|
+
throw new WorkspaceInstructionPolicyOperationReuseError();
|
|
1289
|
+
}
|
|
1290
|
+
return { head: headFromEventRow(existingEvent), event: eventFromRow(existingEvent) };
|
|
1291
|
+
}
|
|
513
1292
|
const targetRow = await getRevisionInTransaction(
|
|
514
1293
|
scopedDb,
|
|
515
1294
|
input.workspaceId,
|
|
@@ -524,7 +1303,11 @@ async function changeActiveRevision(
|
|
|
524
1303
|
};
|
|
525
1304
|
const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, targetIdentity);
|
|
526
1305
|
const currentHead = currentRow ? headFromRow(currentRow) : null;
|
|
527
|
-
if (
|
|
1306
|
+
if (
|
|
1307
|
+
(currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
|
|
1308
|
+
(input.expectedActivationVersion !== undefined &&
|
|
1309
|
+
(currentHead?.activationVersion ?? 0) !== input.expectedActivationVersion)
|
|
1310
|
+
) {
|
|
528
1311
|
throw new WorkspaceInstructionPolicyConflictError(currentHead);
|
|
529
1312
|
}
|
|
530
1313
|
if (currentHead?.revisionId === target.id) {
|
|
@@ -559,6 +1342,8 @@ async function changeActiveRevision(
|
|
|
559
1342
|
const [eventRow] = await scopedDb
|
|
560
1343
|
.insert(schema.workspaceInstructionPolicyActivationEvents)
|
|
561
1344
|
.values({
|
|
1345
|
+
operationId: input.operationId,
|
|
1346
|
+
requestFingerprint,
|
|
562
1347
|
accountId: input.accountId,
|
|
563
1348
|
workspaceId: input.workspaceId,
|
|
564
1349
|
...targetIdentity,
|
|
@@ -613,16 +1398,19 @@ async function changeActiveRevision(
|
|
|
613
1398
|
export async function activateWorkspaceInstructionPolicyRevision(
|
|
614
1399
|
db: Database,
|
|
615
1400
|
input: {
|
|
1401
|
+
operationId?: string;
|
|
616
1402
|
accountId: string;
|
|
617
1403
|
workspaceId: string;
|
|
618
1404
|
revisionId: string;
|
|
619
1405
|
expectedCurrentRevisionId: string | null;
|
|
1406
|
+
expectedActivationVersion?: number;
|
|
620
1407
|
actorSubjectId: string;
|
|
621
1408
|
reason: string;
|
|
622
1409
|
},
|
|
623
1410
|
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
624
1411
|
return await changeActiveRevision(db, {
|
|
625
1412
|
...input,
|
|
1413
|
+
operationId: input.operationId ?? randomUUID(),
|
|
626
1414
|
targetRevisionId: input.revisionId,
|
|
627
1415
|
type: "activate",
|
|
628
1416
|
});
|
|
@@ -631,15 +1419,21 @@ export async function activateWorkspaceInstructionPolicyRevision(
|
|
|
631
1419
|
export async function rollbackWorkspaceInstructionPolicyRevision(
|
|
632
1420
|
db: Database,
|
|
633
1421
|
input: {
|
|
1422
|
+
operationId?: string;
|
|
634
1423
|
accountId: string;
|
|
635
1424
|
workspaceId: string;
|
|
636
1425
|
targetRevisionId: string;
|
|
637
1426
|
expectedCurrentRevisionId: string;
|
|
1427
|
+
expectedActivationVersion?: number;
|
|
638
1428
|
actorSubjectId: string;
|
|
639
1429
|
reason: string;
|
|
640
1430
|
},
|
|
641
1431
|
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
642
|
-
return await changeActiveRevision(db, {
|
|
1432
|
+
return await changeActiveRevision(db, {
|
|
1433
|
+
...input,
|
|
1434
|
+
operationId: input.operationId ?? randomUUID(),
|
|
1435
|
+
type: "rollback",
|
|
1436
|
+
});
|
|
643
1437
|
}
|
|
644
1438
|
|
|
645
1439
|
/**
|