@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.
- package/dist/{chunk-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
- package/dist/chunk-7WTDI7Y3.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
- package/dist/chunk-KW6U54V2.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +107 -5
- package/dist/index.js +7857 -3898
- 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 +1659 -77
- package/dist/schema.js +13 -1
- package/dist/session-control.d.ts +7 -1
- package/dist/session-queue-commands.d.ts +8 -0
- package/dist/session-realtime-context.d.ts +56 -0
- package/dist/session-realtime-ledger.d.ts +188 -0
- package/dist/session-realtime-mirror.d.ts +51 -0
- package/dist/session-realtime-state.d.ts +2 -0
- package/dist/session-realtime-terminal.d.ts +40 -0
- package/dist/session-realtime.d.ts +59 -0
- package/dist/workspace-instruction-policies-schema.d.ts +307 -0
- package/dist/workspace-instruction-policies.d.ts +97 -7
- package/drizzle/0156_slack_reaction_trigger.sql +49 -0
- package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
- package/drizzle/0158_session_realtime_mode.sql +88 -0
- package/drizzle/0159_session_realtime_ledger.sql +198 -0
- package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
- package/drizzle/0161_session_realtime_context_projection.sql +82 -0
- package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
- package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
- package/drizzle/0164_session_realtime_models.sql +28 -0
- 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 +4 -4
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +1033 -101
- package/src/preference-registry.ts +114 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +12 -0
- package/src/schema.ts +486 -43
- package/src/session-control.ts +643 -21
- package/src/session-queue-commands.ts +121 -18
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +276 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +659 -0
- package/src/workspace-instruction-policies-schema.ts +71 -0
- package/src/workspace-instruction-policies.ts +568 -25
- package/dist/chunk-BNGEN5QZ.js.map +0 -1
- package/dist/chunk-CYGFLLMN.js.map +0 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
...
|
|
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 (
|
|
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, {
|
|
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
|
}
|