@runuai/host 0.9.148 → 0.9.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/db/migrations/0024_task_env_mode.sql +9 -0
  2. package/db/migrations/0025_project_env_authority.sql +147 -0
  3. package/db/migrations/0026_project_env_replica.sql +150 -0
  4. package/db/migrations/meta/_journal.json +21 -0
  5. package/db/schema.ts +312 -2
  6. package/images/standard/Dockerfile +44 -6
  7. package/images/standard/container/uai-init +85 -2
  8. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  9. package/lib/agent-cli.ts +18 -1
  10. package/lib/agent.ts +99 -34
  11. package/lib/agents/codex.ts +9 -1
  12. package/lib/agents/cursor.ts +15 -1
  13. package/lib/agents/grok.ts +8 -0
  14. package/lib/agents/kimi.ts +1 -0
  15. package/lib/agents/machine-durable.ts +2 -0
  16. package/lib/agents/opencode.ts +1 -0
  17. package/lib/agents/proc.ts +6 -1
  18. package/lib/agents/transport.ts +9 -5
  19. package/lib/browser-testing.ts +6 -0
  20. package/lib/codex-auth.ts +2 -0
  21. package/lib/credential-store.ts +141 -6
  22. package/lib/engine-accounts.ts +1 -0
  23. package/lib/env-file.ts +7 -0
  24. package/lib/env-schema.ts +841 -51
  25. package/lib/git-diff.ts +1 -0
  26. package/lib/git-identity.ts +2 -0
  27. package/lib/github-tokens.ts +1 -0
  28. package/lib/host-env.ts +98 -18
  29. package/lib/lease.ts +11 -3
  30. package/lib/machine-gateway-tunnel.ts +195 -17
  31. package/lib/machine-project-env-loader.ts +152 -0
  32. package/lib/managed-runtime.ts +270 -51
  33. package/lib/mcp-gateway.ts +777 -26
  34. package/lib/orchestrator.ts +391 -69
  35. package/lib/project-env-authority.ts +1319 -0
  36. package/lib/project-env-feature.ts +74 -0
  37. package/lib/project-env-loader.ts +237 -0
  38. package/lib/project-env-replica.ts +2094 -0
  39. package/lib/project-env-schema-capture.ts +342 -0
  40. package/lib/project-env-staging.ts +148 -0
  41. package/lib/project-env-task-policy.ts +176 -0
  42. package/lib/runtime-archive.ts +79 -16
  43. package/lib/runtime-authority.ts +44 -0
  44. package/lib/runtime-state.ts +175 -10
  45. package/lib/secrets-proxy/address.ts +72 -0
  46. package/lib/secrets-proxy/audit.ts +30 -0
  47. package/lib/secrets-proxy/ca-store.ts +200 -0
  48. package/lib/secrets-proxy/ca.ts +470 -0
  49. package/lib/secrets-proxy/container-steps.ts +138 -0
  50. package/lib/secrets-proxy/der.ts +273 -0
  51. package/lib/secrets-proxy/engine.ts +1633 -0
  52. package/lib/secrets-proxy/host.ts +93 -0
  53. package/lib/secrets-proxy/hostname.ts +60 -0
  54. package/lib/secrets-proxy/placeholders.ts +196 -0
  55. package/lib/secrets-proxy/policy.ts +141 -0
  56. package/lib/secrets-proxy/readiness.ts +148 -0
  57. package/lib/secrets-proxy/runtime.ts +582 -0
  58. package/lib/secrets-proxy/scrub.ts +559 -0
  59. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  60. package/lib/secrets-proxy/task-scripts.ts +262 -0
  61. package/lib/secrets-proxy/task-trust.ts +574 -0
  62. package/lib/secrets-proxy/trust-watch.ts +80 -0
  63. package/lib/shared-files-s3.ts +3 -0
  64. package/lib/sidecar-guard.ts +58 -0
  65. package/lib/sidecar-resume.ts +51 -3
  66. package/lib/standard-image.ts +50 -12
  67. package/lib/task-env-forwarder.ts +228 -0
  68. package/lib/task-env-source.ts +193 -0
  69. package/lib/task-environment/apple-container.ts +45 -11
  70. package/lib/task-environment/docker.ts +51 -12
  71. package/lib/task-environment/index.ts +6 -2
  72. package/lib/task-environment/machine-task-up.ts +258 -25
  73. package/lib/task-environment/machine.ts +80 -22
  74. package/lib/task-environment/registry.ts +3 -2
  75. package/lib/task-environment/self-identity.ts +20 -0
  76. package/lib/task-environment/self.ts +92 -16
  77. package/lib/task-environment/types.ts +63 -0
  78. package/lib/task-gateway-port.ts +22 -0
  79. package/lib/task-gateway-protocol.ts +190 -0
  80. package/lib/task-gateway-recovery.ts +44 -0
  81. package/lib/task-gateway-state.ts +497 -0
  82. package/lib/transcript.ts +1 -0
  83. package/package.json +13 -1
  84. package/scripts/agent/env-hook.sh +626 -0
  85. package/scripts/agent/task-up.sh +476 -27
  86. package/scripts/agent/uai-env-exec.sh +26 -0
  87. package/src/cli.ts +26 -3
  88. package/src/index.ts +273 -19
  89. package/src/main.ts +939 -48
  90. package/src/project-env-latest.ts +130 -0
  91. package/src/project-env-policy.ts +281 -0
  92. package/src/project-env-wording.ts +47 -0
  93. package/src/protocol.ts +2219 -15
@@ -0,0 +1,2094 @@
1
+ /**
2
+ * Durable Uai Cloud task-machine project-env replica (ADR-153).
3
+ *
4
+ * The cloud owns the records. A sidecar keeps a whole-task, value-free
5
+ * replica plus encrypted materialised values. Snapshot pages are written to
6
+ * staging and are never served; only the exact commit marker promotes them.
7
+ * A newer head is first made durable with no plaintext, then its immutable
8
+ * SSM reference is fetched and conditionally installed. Thus a crash can
9
+ * withhold a value, but can never pair an old value with a new policy/head.
10
+ */
11
+
12
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
13
+
14
+ import { and, asc, eq } from "drizzle-orm";
15
+
16
+ import {
17
+ MAX_PROJECT_ENV_ACK_PAGE_BYTES,
18
+ isHostOpId,
19
+ isProjectEnvPolicyEpochs,
20
+ isProjectEnvReplicaApplyFrame,
21
+ isProjectEnvReplicaBeginFrame,
22
+ isProjectEnvReplicaBindingState,
23
+ isProjectEnvReplicaCommitFrame,
24
+ isProjectEnvReplicaPageFrame,
25
+ isProjectEnvReplicaResult,
26
+ sameProjectEnvHead,
27
+ type ProjectEnvBinding,
28
+ type ProjectEnvDeliveryCursor,
29
+ type ProjectEnvReplicaApplyFrame,
30
+ type ProjectEnvReplicaBeginFrame,
31
+ type ProjectEnvReplicaBindingState,
32
+ type ProjectEnvReplicaCommitFrame,
33
+ type ProjectEnvReplicaMaterialization,
34
+ type ProjectEnvReplicaPageFrame,
35
+ type ProjectEnvReplicaResult,
36
+ type ProjectEnvValueRef,
37
+ type ProjectEnvWithholdReason,
38
+ } from "../src/protocol";
39
+ import {
40
+ configuredCredentialStore,
41
+ credentialStorePrefix,
42
+ isParameterPathSegment,
43
+ parameterName,
44
+ type CredentialStore,
45
+ } from "./credential-store";
46
+ import { getDb, schema, type Db } from "./db";
47
+ import {
48
+ noteProjectEnvAuthorityChange,
49
+ noteProjectEnvValueMutation,
50
+ onProjectEnvChange,
51
+ openProjectEnvValueFromStorage,
52
+ projectEnvParameterName,
53
+ sealProjectEnvValueForStorage,
54
+ } from "./host-env";
55
+ import { masterKey } from "./secrets";
56
+ import { parseStoredProjectEnv } from "./user-credentials-store";
57
+
58
+ type ReplicaAuthorityRow =
59
+ typeof schema.hostProjectEnvReplicaAuthority.$inferSelect;
60
+ type ReplicaRecordRow =
61
+ typeof schema.hostProjectEnvReplicaRecords.$inferSelect;
62
+ type ReplicaStageRecordRow =
63
+ typeof schema.hostProjectEnvReplicaStageRecords.$inferSelect;
64
+ type ReplicaReceiptRow =
65
+ typeof schema.hostProjectEnvReplicaReceipts.$inferSelect;
66
+ type ReplicaTransaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
67
+ type ReplicaExecutor = Db | ReplicaTransaction;
68
+
69
+ const RECEIPT_HMAC_DOMAIN = Buffer.from(
70
+ "uai.project_env.replica_receipt.v1\0",
71
+ "utf8",
72
+ );
73
+ const MAX_REASON = "x".repeat(64);
74
+
75
+ export class ProjectEnvReplicaCorruptError extends Error {
76
+ constructor(message: string) {
77
+ super(message);
78
+ this.name = "ProjectEnvReplicaCorruptError";
79
+ }
80
+ }
81
+
82
+ export interface ProjectEnvReplicaStatus {
83
+ activated: boolean;
84
+ gateOpen: boolean;
85
+ attemptId: string | null;
86
+ orgId: string | null;
87
+ appliedThrough: ProjectEnvDeliveryCursor | null;
88
+ policyEpochs: number[];
89
+ projectIds: string[];
90
+ }
91
+
92
+ export interface ProjectEnvReplicaSnapshotEntry {
93
+ state: ProjectEnvReplicaBindingState;
94
+ materialization: ProjectEnvReplicaMaterialization;
95
+ value: string | null;
96
+ }
97
+
98
+ export interface ProjectEnvReplicaSnapshotProject {
99
+ projectId: string;
100
+ recordVersion: number;
101
+ entries: ProjectEnvReplicaSnapshotEntry[];
102
+ }
103
+
104
+ export interface ProjectEnvReplicaSnapshot {
105
+ projects: ProjectEnvReplicaSnapshotProject[];
106
+ }
107
+
108
+ export interface ProjectEnvReplicaValueReader {
109
+ readStaged(name: string): Promise<string | null>;
110
+ readLegacy(
111
+ name: string,
112
+ version: number,
113
+ ): Promise<{ value: string; version: number } | null>;
114
+ }
115
+
116
+ export type ProjectEnvReplicaBeginResult = {
117
+ appliedThrough: ProjectEnvDeliveryCursor;
118
+ policyEpochs: number[];
119
+ };
120
+
121
+ export type ProjectEnvReplicaPageResult =
122
+ | {
123
+ ok: true;
124
+ attemptId: string;
125
+ projectId: string;
126
+ after: string | null;
127
+ recordVersion: number;
128
+ results: ProjectEnvReplicaResult[];
129
+ complete: boolean;
130
+ policyEpochs: number[];
131
+ appliedThrough: ProjectEnvDeliveryCursor;
132
+ }
133
+ | {
134
+ ok: false;
135
+ attemptId: string;
136
+ code: ProjectEnvWithholdReason;
137
+ policyEpochs: number[];
138
+ appliedThrough: ProjectEnvDeliveryCursor;
139
+ };
140
+
141
+ export type ProjectEnvReplicaApplyResult =
142
+ | {
143
+ ok: true;
144
+ replayed: boolean;
145
+ attemptId: string | null;
146
+ projectId: string;
147
+ recordVersion: number;
148
+ result: ProjectEnvReplicaResult;
149
+ policyEpochs: number[];
150
+ appliedThrough: ProjectEnvDeliveryCursor;
151
+ }
152
+ | {
153
+ ok: false;
154
+ replayed: boolean;
155
+ consumed: boolean;
156
+ attemptId: string | null;
157
+ code: ProjectEnvWithholdReason;
158
+ policyEpochs: number[];
159
+ appliedThrough: ProjectEnvDeliveryCursor;
160
+ };
161
+
162
+ export type ProjectEnvReplicaCommitResult =
163
+ | {
164
+ ok: true;
165
+ attemptId: string;
166
+ policyEpochs: number[];
167
+ appliedThrough: ProjectEnvDeliveryCursor;
168
+ }
169
+ | {
170
+ ok: false;
171
+ attemptId: string;
172
+ code: ProjectEnvWithholdReason;
173
+ policyEpochs: number[];
174
+ appliedThrough: ProjectEnvDeliveryCursor;
175
+ };
176
+
177
+ function authorityRow(executor: ReplicaExecutor): ReplicaAuthorityRow {
178
+ const row = executor
179
+ .select()
180
+ .from(schema.hostProjectEnvReplicaAuthority)
181
+ .where(eq(schema.hostProjectEnvReplicaAuthority.id, "replica"))
182
+ .get();
183
+ if (!row) {
184
+ throw new ProjectEnvReplicaCorruptError(
185
+ "project environment replica authority row is missing",
186
+ );
187
+ }
188
+ return row;
189
+ }
190
+
191
+ function policyEpochs(row: ReplicaAuthorityRow): number[] {
192
+ let parsed: unknown;
193
+ try {
194
+ parsed = JSON.parse(row.policyEpochsJson);
195
+ } catch {
196
+ throw new ProjectEnvReplicaCorruptError(
197
+ "stored replica policy epochs are malformed",
198
+ );
199
+ }
200
+ if (!isProjectEnvPolicyEpochs(parsed)) {
201
+ throw new ProjectEnvReplicaCorruptError(
202
+ "stored replica policy epochs are invalid",
203
+ );
204
+ }
205
+ return parsed;
206
+ }
207
+
208
+ function cursor(row: ReplicaAuthorityRow): ProjectEnvDeliveryCursor | null {
209
+ if (!row.activated) return null;
210
+ if (
211
+ !isHostOpId(row.streamId) ||
212
+ !Number.isSafeInteger(row.appliedSequence) ||
213
+ row.appliedSequence < 0
214
+ ) {
215
+ throw new ProjectEnvReplicaCorruptError(
216
+ "stored replica delivery cursor is invalid",
217
+ );
218
+ }
219
+ return { streamId: row.streamId, sequence: row.appliedSequence };
220
+ }
221
+
222
+ function requiredCursor(row: ReplicaAuthorityRow): ProjectEnvDeliveryCursor {
223
+ const value = cursor(row);
224
+ if (value === null) {
225
+ throw new ProjectEnvReplicaCorruptError(
226
+ "project environment replica has not been activated",
227
+ );
228
+ }
229
+ return value;
230
+ }
231
+
232
+ function authorityOrgId(row: ReplicaAuthorityRow): string {
233
+ if (!row.activated || row.orgId === null || !isParameterPathSegment(row.orgId)) {
234
+ throw new ProjectEnvReplicaCorruptError(
235
+ "stored replica organization is invalid",
236
+ );
237
+ }
238
+ return row.orgId;
239
+ }
240
+
241
+ function parseJson(value: string | null, what: string): unknown {
242
+ if (value === null) return null;
243
+ try {
244
+ return JSON.parse(value) as unknown;
245
+ } catch {
246
+ throw new ProjectEnvReplicaCorruptError(`stored ${what} is malformed`);
247
+ }
248
+ }
249
+
250
+ function rowState(
251
+ row: ReplicaRecordRow | ReplicaStageRecordRow,
252
+ ): ProjectEnvReplicaBindingState {
253
+ const binding = parseJson(row.bindingJson, "replica binding") as
254
+ | ProjectEnvBinding
255
+ | null;
256
+ const valueRef = parseJson(row.valueRefJson, "replica value reference") as
257
+ | ProjectEnvValueRef
258
+ | null;
259
+ const state: unknown = {
260
+ key: row.key,
261
+ head: row.tombstone
262
+ ? {
263
+ revision: row.revision,
264
+ generation: row.generation,
265
+ tombstone: true,
266
+ valueRef: null,
267
+ }
268
+ : {
269
+ revision: row.revision,
270
+ generation: row.generation,
271
+ tombstone: false,
272
+ valueRef,
273
+ },
274
+ binding,
275
+ policyEpoch: row.policyEpoch,
276
+ listingGeneration: row.listingGeneration,
277
+ };
278
+ if (!isProjectEnvReplicaBindingState(state)) {
279
+ throw new ProjectEnvReplicaCorruptError(
280
+ `stored replica record is invalid (${row.projectId}/${row.key})`,
281
+ );
282
+ }
283
+ return state;
284
+ }
285
+
286
+ function rowMaterialization(
287
+ row: ReplicaRecordRow | ReplicaStageRecordRow,
288
+ ): ProjectEnvReplicaMaterialization {
289
+ const value: unknown =
290
+ row.materialization === "installed"
291
+ ? { kind: "installed" }
292
+ : row.materialization === "withheld"
293
+ ? { kind: "withheld", reason: row.materializationReason }
294
+ : row.materialization === "unavailable"
295
+ ? { kind: "unavailable", reason: row.materializationReason }
296
+ : null;
297
+ const result: unknown = { state: rowState(row), materialization: value };
298
+ if (!isProjectEnvReplicaResult(result)) {
299
+ throw new ProjectEnvReplicaCorruptError(
300
+ `stored replica materialization is invalid (${row.projectId}/${row.key})`,
301
+ );
302
+ }
303
+ return (result as ProjectEnvReplicaResult).materialization;
304
+ }
305
+
306
+ function rowResult(
307
+ row: ReplicaRecordRow | ReplicaStageRecordRow,
308
+ ): ProjectEnvReplicaResult {
309
+ const result: unknown = {
310
+ state: rowState(row),
311
+ materialization: rowMaterialization(row),
312
+ };
313
+ if (!isProjectEnvReplicaResult(result)) {
314
+ throw new ProjectEnvReplicaCorruptError(
315
+ `stored replica outcome is invalid (${row.projectId}/${row.key})`,
316
+ );
317
+ }
318
+ return result;
319
+ }
320
+
321
+ function materializationColumns(
322
+ materialization: ProjectEnvReplicaMaterialization,
323
+ ): { materialization: string; materializationReason: string | null } {
324
+ return {
325
+ materialization: materialization.kind,
326
+ materializationReason:
327
+ materialization.kind === "installed" ? null : materialization.reason,
328
+ };
329
+ }
330
+
331
+ function stateColumns(state: ProjectEnvReplicaBindingState) {
332
+ return {
333
+ revision: state.head.revision,
334
+ generation: state.head.generation,
335
+ tombstone: state.head.tombstone,
336
+ valueRefJson:
337
+ state.head.valueRef === null
338
+ ? null
339
+ : JSON.stringify(state.head.valueRef),
340
+ bindingJson:
341
+ state.binding === null ? null : JSON.stringify(state.binding),
342
+ policyEpoch: state.policyEpoch,
343
+ listingGeneration: state.listingGeneration,
344
+ };
345
+ }
346
+
347
+ function sortedProjectIds(executor: ReplicaExecutor): string[] {
348
+ return executor
349
+ .select({ projectId: schema.hostProjectEnvReplicaProjects.projectId })
350
+ .from(schema.hostProjectEnvReplicaProjects)
351
+ .orderBy(asc(schema.hostProjectEnvReplicaProjects.projectId))
352
+ .all()
353
+ .map((row) => row.projectId);
354
+ }
355
+
356
+ function sameStrings(left: readonly string[], right: readonly string[]): boolean {
357
+ return (
358
+ left.length === right.length &&
359
+ left.every((value, index) => value === right[index])
360
+ );
361
+ }
362
+
363
+ function notifyProjects(projectIds: Iterable<string>): void {
364
+ const uniqueProjectIds = new Set(projectIds);
365
+ for (const projectId of uniqueProjectIds) {
366
+ noteProjectEnvValueMutation(projectId);
367
+ }
368
+ // An empty task still needs to wake admission when begin/commit changes its
369
+ // authority gate. There is no project revision to bump in that case.
370
+ if (uniqueProjectIds.size === 0) noteProjectEnvAuthorityChange();
371
+ }
372
+
373
+ function exactJson(value: unknown): string {
374
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
375
+ if (Array.isArray(value)) return `[${value.map(exactJson).join(",")}]`;
376
+ const object = value as Record<string, unknown>;
377
+ return `{${Object.keys(object)
378
+ .sort()
379
+ .map((key) => `${JSON.stringify(key)}:${exactJson(object[key])}`)
380
+ .join(",")}}`;
381
+ }
382
+
383
+ function requestMac(frame: ProjectEnvReplicaApplyFrame): string {
384
+ const semanticRequest = {
385
+ kind: frame.kind,
386
+ projectId: frame.projectId,
387
+ recordVersion: frame.recordVersion,
388
+ state: frame.state,
389
+ delivery: frame.delivery,
390
+ };
391
+ return createHmac("sha256", masterKey())
392
+ .update(RECEIPT_HMAC_DOMAIN)
393
+ .update(exactJson(semanticRequest))
394
+ .digest("hex");
395
+ }
396
+
397
+ function sameMac(left: string, right: string): boolean {
398
+ if (!/^[0-9a-f]{64}$/.test(left) || !/^[0-9a-f]{64}$/.test(right)) {
399
+ return false;
400
+ }
401
+ return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
402
+ }
403
+
404
+ function sameState(
405
+ left: ProjectEnvReplicaBindingState,
406
+ right: ProjectEnvReplicaBindingState,
407
+ ): boolean {
408
+ return exactJson(left) === exactJson(right);
409
+ }
410
+
411
+ type StateOrder = "same" | "newer" | "older" | "conflict";
412
+
413
+ function stateOrder(
414
+ incoming: ProjectEnvReplicaBindingState,
415
+ current: ProjectEnvReplicaBindingState,
416
+ ): StateOrder {
417
+ const revisionDelta = incoming.head.revision - current.head.revision;
418
+ const generationDelta = incoming.head.generation - current.head.generation;
419
+ if (revisionDelta < 0 && generationDelta <= 0) return "older";
420
+ if (generationDelta < 0 && revisionDelta <= 0) return "older";
421
+ if (revisionDelta < 0 || generationDelta < 0) return "conflict";
422
+ if (revisionDelta === 0 && generationDelta === 0) {
423
+ if (!sameProjectEnvHead(incoming.head, current.head)) return "conflict";
424
+ if (
425
+ exactJson(incoming.binding) !== exactJson(current.binding) ||
426
+ incoming.policyEpoch !== current.policyEpoch
427
+ ) {
428
+ return "conflict";
429
+ }
430
+ if (incoming.listingGeneration < current.listingGeneration) return "older";
431
+ if (incoming.listingGeneration === current.listingGeneration) {
432
+ return sameState(incoming, current) ? "same" : "conflict";
433
+ }
434
+ return "newer";
435
+ }
436
+ if (incoming.listingGeneration < current.listingGeneration) return "conflict";
437
+ // One counter staying put constrains the fields that counter protects.
438
+ if (
439
+ revisionDelta === 0 &&
440
+ (incoming.head.tombstone !== current.head.tombstone ||
441
+ exactJson(incoming.binding) !== exactJson(current.binding) ||
442
+ incoming.policyEpoch !== current.policyEpoch)
443
+ ) {
444
+ return "conflict";
445
+ }
446
+ if (
447
+ generationDelta === 0 &&
448
+ (incoming.head.tombstone !== current.head.tombstone ||
449
+ exactJson(incoming.head.valueRef) !== exactJson(current.head.valueRef))
450
+ ) {
451
+ return "conflict";
452
+ }
453
+ return "newer";
454
+ }
455
+
456
+ /** One cloud record-version step can perform exactly one head-table move. */
457
+ function isReachableRecordAdvance(
458
+ incoming: ProjectEnvReplicaBindingState,
459
+ current: ProjectEnvReplicaBindingState | null,
460
+ ): boolean {
461
+ if (current === null) {
462
+ return (
463
+ (incoming.head.revision === 1 &&
464
+ incoming.head.generation === 1 &&
465
+ !incoming.head.tombstone) ||
466
+ (incoming.head.revision === 1 &&
467
+ incoming.head.generation === 0 &&
468
+ incoming.head.tombstone)
469
+ );
470
+ }
471
+ const revision = incoming.head.revision - current.head.revision;
472
+ const generation = incoming.head.generation - current.head.generation;
473
+ if (incoming.listingGeneration < current.listingGeneration) return false;
474
+ if (revision === 0 && generation === 0) {
475
+ return (
476
+ sameProjectEnvHead(incoming.head, current.head) &&
477
+ exactJson(incoming.binding) === exactJson(current.binding) &&
478
+ incoming.policyEpoch === current.policyEpoch &&
479
+ incoming.listingGeneration > current.listingGeneration
480
+ );
481
+ }
482
+ // A live rotation moves only generation. Policy and binding are retained.
483
+ if (revision === 0 && generation === 1) {
484
+ return (
485
+ !current.head.tombstone &&
486
+ !incoming.head.tombstone &&
487
+ exactJson(incoming.binding) === exactJson(current.binding) &&
488
+ incoming.policyEpoch === current.policyEpoch
489
+ );
490
+ }
491
+ // Approve / epoch upgrade moves only revision and retains the value ref.
492
+ if (revision === 1 && generation === 0) {
493
+ return (
494
+ !current.head.tombstone &&
495
+ !incoming.head.tombstone &&
496
+ exactJson(incoming.head.valueRef) === exactJson(current.head.valueRef)
497
+ );
498
+ }
499
+ // Delete/removal, save-after-delete and first-save-on-fence move both.
500
+ if (revision === 1 && generation === 1) {
501
+ if (current.head.generation === 0) {
502
+ return current.head.tombstone && !incoming.head.tombstone;
503
+ }
504
+ return (
505
+ current.head.tombstone !== incoming.head.tombstone &&
506
+ exactJson(incoming.binding) === exactJson(current.binding) &&
507
+ incoming.policyEpoch === current.policyEpoch
508
+ );
509
+ }
510
+ return false;
511
+ }
512
+
513
+ function retainedExisting(
514
+ row: ReplicaRecordRow | ReplicaStageRecordRow,
515
+ epochs: readonly number[],
516
+ ): { result: ProjectEnvReplicaResult; fetch: boolean; changed: false } {
517
+ const state = rowState(row);
518
+ return {
519
+ result: rowResult(row),
520
+ fetch:
521
+ !state.head.tombstone &&
522
+ row.materialization !== "installed" &&
523
+ desiredMaterialization(state, epochs).kind === "unavailable",
524
+ changed: false,
525
+ };
526
+ }
527
+
528
+ function desiredMaterialization(
529
+ state: ProjectEnvReplicaBindingState,
530
+ epochs: readonly number[],
531
+ ): ProjectEnvReplicaMaterialization {
532
+ if (state.head.tombstone) {
533
+ return { kind: "withheld", reason: "tombstone" };
534
+ }
535
+ if (state.policyEpoch === null || !epochs.includes(state.policyEpoch)) {
536
+ return { kind: "withheld", reason: "policy_epoch_unknown" };
537
+ }
538
+ if (state.binding?.kind === "omit") {
539
+ return { kind: "withheld", reason: "binding_omits" };
540
+ }
541
+ if (state.binding?.kind === "withhold") {
542
+ return { kind: "withheld", reason: state.binding.reason };
543
+ }
544
+ return { kind: "unavailable", reason: "materialization_pending" };
545
+ }
546
+
547
+ function putStageRecord(
548
+ tx: ReplicaTransaction,
549
+ attemptId: string,
550
+ projectId: string,
551
+ state: ProjectEnvReplicaBindingState,
552
+ materialization: ProjectEnvReplicaMaterialization,
553
+ present: boolean,
554
+ ): void {
555
+ tx.insert(schema.hostProjectEnvReplicaStageRecords)
556
+ .values({
557
+ attemptId,
558
+ projectId,
559
+ key: state.key,
560
+ ...stateColumns(state),
561
+ ...materializationColumns(materialization),
562
+ present,
563
+ updatedAt: Date.now(),
564
+ })
565
+ .onConflictDoUpdate({
566
+ target: [
567
+ schema.hostProjectEnvReplicaStageRecords.attemptId,
568
+ schema.hostProjectEnvReplicaStageRecords.projectId,
569
+ schema.hostProjectEnvReplicaStageRecords.key,
570
+ ],
571
+ set: {
572
+ ...stateColumns(state),
573
+ ...materializationColumns(materialization),
574
+ present,
575
+ updatedAt: Date.now(),
576
+ },
577
+ })
578
+ .run();
579
+ }
580
+
581
+ function putLiveRecord(
582
+ tx: ReplicaTransaction,
583
+ projectId: string,
584
+ state: ProjectEnvReplicaBindingState,
585
+ materialization: ProjectEnvReplicaMaterialization,
586
+ ): void {
587
+ tx.insert(schema.hostProjectEnvReplicaRecords)
588
+ .values({
589
+ projectId,
590
+ key: state.key,
591
+ ...stateColumns(state),
592
+ ...materializationColumns(materialization),
593
+ updatedAt: Date.now(),
594
+ })
595
+ .onConflictDoUpdate({
596
+ target: [
597
+ schema.hostProjectEnvReplicaRecords.projectId,
598
+ schema.hostProjectEnvReplicaRecords.key,
599
+ ],
600
+ set: {
601
+ ...stateColumns(state),
602
+ ...materializationColumns(materialization),
603
+ updatedAt: Date.now(),
604
+ },
605
+ })
606
+ .run();
607
+ }
608
+
609
+ function stageRecord(
610
+ tx: ReplicaTransaction,
611
+ attemptId: string,
612
+ projectId: string,
613
+ key: string,
614
+ ): ReplicaStageRecordRow | null {
615
+ return (
616
+ tx
617
+ .select()
618
+ .from(schema.hostProjectEnvReplicaStageRecords)
619
+ .where(
620
+ and(
621
+ eq(schema.hostProjectEnvReplicaStageRecords.attemptId, attemptId),
622
+ eq(schema.hostProjectEnvReplicaStageRecords.projectId, projectId),
623
+ eq(schema.hostProjectEnvReplicaStageRecords.key, key),
624
+ ),
625
+ )
626
+ .get() ?? null
627
+ );
628
+ }
629
+
630
+ function liveRecord(
631
+ tx: ReplicaTransaction,
632
+ projectId: string,
633
+ key: string,
634
+ ): ReplicaRecordRow | null {
635
+ return (
636
+ tx
637
+ .select()
638
+ .from(schema.hostProjectEnvReplicaRecords)
639
+ .where(
640
+ and(
641
+ eq(schema.hostProjectEnvReplicaRecords.projectId, projectId),
642
+ eq(schema.hostProjectEnvReplicaRecords.key, key),
643
+ ),
644
+ )
645
+ .get() ?? null
646
+ );
647
+ }
648
+
649
+ function resetStagedProject(
650
+ tx: ReplicaTransaction,
651
+ attemptId: string,
652
+ projectId: string,
653
+ ): void {
654
+ tx.delete(schema.hostProjectEnvReplicaStageValues)
655
+ .where(
656
+ and(
657
+ eq(schema.hostProjectEnvReplicaStageValues.attemptId, attemptId),
658
+ eq(schema.hostProjectEnvReplicaStageValues.projectId, projectId),
659
+ ),
660
+ )
661
+ .run();
662
+ tx.delete(schema.hostProjectEnvReplicaStageRecords)
663
+ .where(
664
+ and(
665
+ eq(schema.hostProjectEnvReplicaStageRecords.attemptId, attemptId),
666
+ eq(schema.hostProjectEnvReplicaStageRecords.projectId, projectId),
667
+ ),
668
+ )
669
+ .run();
670
+ const records = tx
671
+ .select()
672
+ .from(schema.hostProjectEnvReplicaRecords)
673
+ .where(eq(schema.hostProjectEnvReplicaRecords.projectId, projectId))
674
+ .all();
675
+ for (const row of records) {
676
+ putStageRecord(
677
+ tx,
678
+ attemptId,
679
+ projectId,
680
+ rowState(row),
681
+ rowMaterialization(row),
682
+ false,
683
+ );
684
+ }
685
+ const values = tx
686
+ .select()
687
+ .from(schema.hostProjectEnvReplicaValues)
688
+ .where(eq(schema.hostProjectEnvReplicaValues.projectId, projectId))
689
+ .all();
690
+ for (const row of values) {
691
+ tx.insert(schema.hostProjectEnvReplicaStageValues)
692
+ .values({ attemptId, ...row })
693
+ .run();
694
+ }
695
+ }
696
+
697
+ function retainStageState(
698
+ tx: ReplicaTransaction,
699
+ attemptId: string,
700
+ projectId: string,
701
+ incoming: ProjectEnvReplicaBindingState,
702
+ epochs: readonly number[],
703
+ ): { result: ProjectEnvReplicaResult; fetch: boolean } {
704
+ const stored = stageRecord(tx, attemptId, projectId, incoming.key);
705
+ if (stored !== null) {
706
+ const current = rowState(stored);
707
+ const order = stateOrder(incoming, current);
708
+ if (order === "older" || order === "conflict") {
709
+ putStageRecord(
710
+ tx,
711
+ attemptId,
712
+ projectId,
713
+ current,
714
+ rowMaterialization(stored),
715
+ true,
716
+ );
717
+ const retained = stageRecord(tx, attemptId, projectId, incoming.key)!;
718
+ return {
719
+ result: rowResult(retained),
720
+ fetch:
721
+ !current.head.tombstone &&
722
+ retained.materialization !== "installed" &&
723
+ desiredMaterialization(current, epochs).kind === "unavailable",
724
+ };
725
+ }
726
+ if (order === "same") {
727
+ const desired = desiredMaterialization(current, epochs);
728
+ const keep =
729
+ stored.materialization === "installed" || desired.kind === "withheld"
730
+ ? desired.kind === "withheld"
731
+ ? desired
732
+ : rowMaterialization(stored)
733
+ : desired;
734
+ if (keep.kind !== "installed") {
735
+ tx.delete(schema.hostProjectEnvReplicaStageValues)
736
+ .where(
737
+ and(
738
+ eq(schema.hostProjectEnvReplicaStageValues.attemptId, attemptId),
739
+ eq(schema.hostProjectEnvReplicaStageValues.projectId, projectId),
740
+ eq(schema.hostProjectEnvReplicaStageValues.key, incoming.key),
741
+ ),
742
+ )
743
+ .run();
744
+ }
745
+ putStageRecord(tx, attemptId, projectId, current, keep, true);
746
+ return {
747
+ result: rowResult(stageRecord(tx, attemptId, projectId, incoming.key)!),
748
+ fetch: keep.kind === "unavailable",
749
+ };
750
+ }
751
+ }
752
+ const desired = desiredMaterialization(incoming, epochs);
753
+ tx.delete(schema.hostProjectEnvReplicaStageValues)
754
+ .where(
755
+ and(
756
+ eq(schema.hostProjectEnvReplicaStageValues.attemptId, attemptId),
757
+ eq(schema.hostProjectEnvReplicaStageValues.projectId, projectId),
758
+ eq(schema.hostProjectEnvReplicaStageValues.key, incoming.key),
759
+ ),
760
+ )
761
+ .run();
762
+ putStageRecord(tx, attemptId, projectId, incoming, desired, true);
763
+ return {
764
+ result: rowResult(stageRecord(tx, attemptId, projectId, incoming.key)!),
765
+ fetch: desired.kind === "unavailable",
766
+ };
767
+ }
768
+
769
+ function retainLiveState(
770
+ tx: ReplicaTransaction,
771
+ projectId: string,
772
+ incoming: ProjectEnvReplicaBindingState,
773
+ epochs: readonly number[],
774
+ ): { result: ProjectEnvReplicaResult; fetch: boolean; changed: boolean } {
775
+ const stored = liveRecord(tx, projectId, incoming.key);
776
+ if (stored !== null) {
777
+ const current = rowState(stored);
778
+ const order = stateOrder(incoming, current);
779
+ if (order === "older" || order === "conflict") {
780
+ return {
781
+ result: rowResult(stored),
782
+ fetch:
783
+ !current.head.tombstone &&
784
+ stored.materialization !== "installed" &&
785
+ desiredMaterialization(current, epochs).kind === "unavailable",
786
+ changed: false,
787
+ };
788
+ }
789
+ if (order === "same") {
790
+ const desired = desiredMaterialization(current, epochs);
791
+ if (stored.materialization === "installed" && desired.kind !== "withheld") {
792
+ return { result: rowResult(stored), fetch: false, changed: false };
793
+ }
794
+ tx.delete(schema.hostProjectEnvReplicaValues)
795
+ .where(
796
+ and(
797
+ eq(schema.hostProjectEnvReplicaValues.projectId, projectId),
798
+ eq(schema.hostProjectEnvReplicaValues.key, incoming.key),
799
+ ),
800
+ )
801
+ .run();
802
+ putLiveRecord(tx, projectId, current, desired);
803
+ return {
804
+ result: rowResult(liveRecord(tx, projectId, incoming.key)!),
805
+ fetch: desired.kind === "unavailable",
806
+ changed: true,
807
+ };
808
+ }
809
+ }
810
+ const desired = desiredMaterialization(incoming, epochs);
811
+ tx.delete(schema.hostProjectEnvReplicaValues)
812
+ .where(
813
+ and(
814
+ eq(schema.hostProjectEnvReplicaValues.projectId, projectId),
815
+ eq(schema.hostProjectEnvReplicaValues.key, incoming.key),
816
+ ),
817
+ )
818
+ .run();
819
+ putLiveRecord(tx, projectId, incoming, desired);
820
+ return {
821
+ result: rowResult(liveRecord(tx, projectId, incoming.key)!),
822
+ fetch: desired.kind === "unavailable",
823
+ changed: true,
824
+ };
825
+ }
826
+
827
+ function defaultValueReader(): ProjectEnvReplicaValueReader | null {
828
+ const store = configuredCredentialStore();
829
+ if (store === null) return null;
830
+ return {
831
+ readStaged: (name) => store.get(name),
832
+ readLegacy: async (name, version) => {
833
+ if (store.getVersion === undefined) return null;
834
+ const found = await store.getVersion(name, version);
835
+ return found === null
836
+ ? null
837
+ : { value: found.value, version: found.version };
838
+ },
839
+ };
840
+ }
841
+
842
+ export function projectEnvReplicaStagedParameterName(
843
+ projectId: string,
844
+ key: string,
845
+ stagingId: string,
846
+ ): string {
847
+ const digest = createHash("sha256").update(key, "utf8").digest("hex");
848
+ if (
849
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
850
+ stagingId,
851
+ )
852
+ ) {
853
+ throw new Error("invalid staged project environment identity");
854
+ }
855
+ return parameterName(
856
+ credentialStorePrefix(),
857
+ "projects",
858
+ projectId,
859
+ "env-values",
860
+ digest,
861
+ stagingId,
862
+ );
863
+ }
864
+
865
+ function exactObject(
866
+ value: unknown,
867
+ keys: readonly string[],
868
+ ): value is Record<string, unknown> {
869
+ return (
870
+ value !== null &&
871
+ typeof value === "object" &&
872
+ !Array.isArray(value) &&
873
+ Object.keys(value).length === keys.length &&
874
+ keys.every((key) => Object.hasOwn(value, key))
875
+ );
876
+ }
877
+
878
+ async function readMaterializedValue(
879
+ orgId: string,
880
+ projectId: string,
881
+ state: ProjectEnvReplicaBindingState,
882
+ reader: ProjectEnvReplicaValueReader | null,
883
+ ): Promise<{ value: string } | { reason: string }> {
884
+ if (state.head.tombstone || state.head.valueRef === null) {
885
+ return { reason: "tombstone" };
886
+ }
887
+ if (reader === null) return { reason: "credential_store_unavailable" };
888
+ const ref = state.head.valueRef;
889
+ try {
890
+ if (ref.kind === "staged") {
891
+ const name = projectEnvReplicaStagedParameterName(
892
+ projectId,
893
+ state.key,
894
+ ref.stagingId,
895
+ );
896
+ const raw = await reader.readStaged(name);
897
+ if (raw === null) return { reason: "value_missing" };
898
+ const parsed: unknown = JSON.parse(raw);
899
+ if (
900
+ !exactObject(parsed, [
901
+ "v",
902
+ "kind",
903
+ "orgId",
904
+ "projectId",
905
+ "key",
906
+ "stagingId",
907
+ "value",
908
+ ]) ||
909
+ parsed.v !== 2 ||
910
+ parsed.kind !== "project-env" ||
911
+ parsed.orgId !== orgId ||
912
+ parsed.projectId !== projectId ||
913
+ parsed.key !== state.key ||
914
+ parsed.stagingId !== ref.stagingId ||
915
+ typeof parsed.value !== "string"
916
+ ) {
917
+ return { reason: "value_envelope_mismatch" };
918
+ }
919
+ return { value: parsed.value };
920
+ }
921
+ const name = projectEnvParameterName(projectId, state.key);
922
+ if (name === null) return { reason: "value_reference_invalid" };
923
+ const found = await reader.readLegacy(name, ref.ssmVersion);
924
+ if (found === null) return { reason: "value_missing" };
925
+ if (found.version !== ref.ssmVersion) {
926
+ return { reason: "value_version_mismatch" };
927
+ }
928
+ const envelope = parseStoredProjectEnv(found.value);
929
+ if (envelope.projectId !== projectId || envelope.key !== state.key) {
930
+ return { reason: "value_envelope_mismatch" };
931
+ }
932
+ return { value: envelope.value };
933
+ } catch {
934
+ return { reason: "value_unavailable" };
935
+ }
936
+ }
937
+
938
+ async function materializeStage(
939
+ attemptId: string,
940
+ projectId: string,
941
+ key: string,
942
+ reader: ProjectEnvReplicaValueReader | null,
943
+ ): Promise<void> {
944
+ const before = getDb().transaction((tx) => {
945
+ const authority = authorityRow(tx);
946
+ if (authority.attemptId !== attemptId) return null;
947
+ const row = stageRecord(tx, attemptId, projectId, key);
948
+ if (row === null) return null;
949
+ const state = rowState(row);
950
+ return row.materialization === "unavailable"
951
+ ? { state, orgId: authorityOrgId(authority) }
952
+ : null;
953
+ });
954
+ if (before === null) return;
955
+ const fetched = await readMaterializedValue(
956
+ before.orgId,
957
+ projectId,
958
+ before.state,
959
+ reader,
960
+ );
961
+ getDb().transaction((tx) => {
962
+ const authority = authorityRow(tx);
963
+ if (authority.attemptId !== attemptId) return;
964
+ const current = stageRecord(tx, attemptId, projectId, key);
965
+ if (current === null || !sameState(rowState(current), before.state)) return;
966
+ if ("value" in fetched) {
967
+ tx.insert(schema.hostProjectEnvReplicaStageValues)
968
+ .values({
969
+ attemptId,
970
+ projectId,
971
+ key,
972
+ revision: before.state.head.revision,
973
+ generation: before.state.head.generation,
974
+ valueRefJson: JSON.stringify(before.state.head.valueRef),
975
+ valueEnc: sealProjectEnvValueForStorage(fetched.value),
976
+ updatedAt: Date.now(),
977
+ })
978
+ .onConflictDoUpdate({
979
+ target: [
980
+ schema.hostProjectEnvReplicaStageValues.attemptId,
981
+ schema.hostProjectEnvReplicaStageValues.projectId,
982
+ schema.hostProjectEnvReplicaStageValues.key,
983
+ ],
984
+ set: {
985
+ revision: before.state.head.revision,
986
+ generation: before.state.head.generation,
987
+ valueRefJson: JSON.stringify(before.state.head.valueRef),
988
+ valueEnc: sealProjectEnvValueForStorage(fetched.value),
989
+ updatedAt: Date.now(),
990
+ },
991
+ })
992
+ .run();
993
+ putStageRecord(
994
+ tx,
995
+ attemptId,
996
+ projectId,
997
+ before.state,
998
+ { kind: "installed" },
999
+ current.present,
1000
+ );
1001
+ } else {
1002
+ tx.delete(schema.hostProjectEnvReplicaStageValues)
1003
+ .where(
1004
+ and(
1005
+ eq(schema.hostProjectEnvReplicaStageValues.attemptId, attemptId),
1006
+ eq(schema.hostProjectEnvReplicaStageValues.projectId, projectId),
1007
+ eq(schema.hostProjectEnvReplicaStageValues.key, key),
1008
+ ),
1009
+ )
1010
+ .run();
1011
+ putStageRecord(
1012
+ tx,
1013
+ attemptId,
1014
+ projectId,
1015
+ before.state,
1016
+ { kind: "unavailable", reason: fetched.reason },
1017
+ current.present,
1018
+ );
1019
+ }
1020
+ });
1021
+ }
1022
+
1023
+ async function materializeLive(
1024
+ projectId: string,
1025
+ key: string,
1026
+ reader: ProjectEnvReplicaValueReader | null,
1027
+ ): Promise<boolean> {
1028
+ const before = getDb().transaction((tx) => {
1029
+ const authority = authorityRow(tx);
1030
+ const row = liveRecord(tx, projectId, key);
1031
+ if (row === null || row.materialization !== "unavailable") return null;
1032
+ return { state: rowState(row), orgId: authorityOrgId(authority) };
1033
+ });
1034
+ if (before === null) return false;
1035
+ const fetched = await readMaterializedValue(
1036
+ before.orgId,
1037
+ projectId,
1038
+ before.state,
1039
+ reader,
1040
+ );
1041
+ const changed = getDb().transaction((tx) => {
1042
+ const current = liveRecord(tx, projectId, key);
1043
+ if (current === null || !sameState(rowState(current), before.state)) return false;
1044
+ if ("value" in fetched) {
1045
+ tx.insert(schema.hostProjectEnvReplicaValues)
1046
+ .values({
1047
+ projectId,
1048
+ key,
1049
+ revision: before.state.head.revision,
1050
+ generation: before.state.head.generation,
1051
+ valueRefJson: JSON.stringify(before.state.head.valueRef),
1052
+ valueEnc: sealProjectEnvValueForStorage(fetched.value),
1053
+ updatedAt: Date.now(),
1054
+ })
1055
+ .onConflictDoUpdate({
1056
+ target: [
1057
+ schema.hostProjectEnvReplicaValues.projectId,
1058
+ schema.hostProjectEnvReplicaValues.key,
1059
+ ],
1060
+ set: {
1061
+ revision: before.state.head.revision,
1062
+ generation: before.state.head.generation,
1063
+ valueRefJson: JSON.stringify(before.state.head.valueRef),
1064
+ valueEnc: sealProjectEnvValueForStorage(fetched.value),
1065
+ updatedAt: Date.now(),
1066
+ },
1067
+ })
1068
+ .run();
1069
+ putLiveRecord(tx, projectId, before.state, { kind: "installed" });
1070
+ } else {
1071
+ tx.delete(schema.hostProjectEnvReplicaValues)
1072
+ .where(
1073
+ and(
1074
+ eq(schema.hostProjectEnvReplicaValues.projectId, projectId),
1075
+ eq(schema.hostProjectEnvReplicaValues.key, key),
1076
+ ),
1077
+ )
1078
+ .run();
1079
+ putLiveRecord(tx, projectId, before.state, {
1080
+ kind: "unavailable",
1081
+ reason: fetched.reason,
1082
+ });
1083
+ }
1084
+ return true;
1085
+ });
1086
+ if (changed) noteProjectEnvValueMutation(projectId);
1087
+ return changed;
1088
+ }
1089
+
1090
+ function pageFits(
1091
+ frame: ProjectEnvReplicaPageFrame,
1092
+ states: readonly ProjectEnvReplicaBindingState[],
1093
+ policy: readonly number[],
1094
+ appliedThrough: ProjectEnvDeliveryCursor,
1095
+ ): boolean {
1096
+ const results: ProjectEnvReplicaResult[] = states.map((state) => {
1097
+ const candidate: unknown = {
1098
+ state,
1099
+ materialization: state.head.tombstone
1100
+ ? { kind: "withheld", reason: MAX_REASON }
1101
+ : { kind: "unavailable", reason: MAX_REASON },
1102
+ };
1103
+ if (!isProjectEnvReplicaResult(candidate)) {
1104
+ throw new ProjectEnvReplicaCorruptError(
1105
+ "valid replica state could not form a page result",
1106
+ );
1107
+ }
1108
+ return candidate;
1109
+ });
1110
+ const ack = {
1111
+ kind: "env.replica.ack",
1112
+ opId: frame.opId,
1113
+ ok: true,
1114
+ phase: "page",
1115
+ attemptId: frame.attemptId,
1116
+ projectId: frame.projectId,
1117
+ after: frame.after,
1118
+ recordVersion: frame.recordVersion,
1119
+ results,
1120
+ complete: frame.complete,
1121
+ policyEpochs: policy,
1122
+ appliedThrough,
1123
+ };
1124
+ return Buffer.byteLength(JSON.stringify(ack), "utf8") <= MAX_PROJECT_ENV_ACK_PAGE_BYTES;
1125
+ }
1126
+
1127
+ /** Close the gate and create an exact whole-task staging view. */
1128
+ export function beginProjectEnvReplica(
1129
+ frame: ProjectEnvReplicaBeginFrame,
1130
+ keptPolicyEpochs: readonly number[],
1131
+ orgId: string,
1132
+ ): ProjectEnvReplicaBeginResult {
1133
+ if (
1134
+ !isProjectEnvReplicaBeginFrame(frame) ||
1135
+ !isProjectEnvPolicyEpochs(keptPolicyEpochs) ||
1136
+ !isParameterPathSegment(orgId)
1137
+ ) {
1138
+ throw new Error("invalid project environment replica begin");
1139
+ }
1140
+ const beforeProjects = sortedProjectIds(getDb());
1141
+ const result = getDb().transaction((tx) => {
1142
+ const before = authorityRow(tx);
1143
+ if (before.activated && before.orgId !== orgId) {
1144
+ throw new ProjectEnvReplicaCorruptError(
1145
+ "replica organization cannot change after activation",
1146
+ );
1147
+ }
1148
+ const changedStream = before.streamId !== frame.attempt.streamId;
1149
+ if (changedStream) {
1150
+ // Receipts only deduplicate one stream. A whole-task replica starts a
1151
+ // fresh stream for each incarnation/reconnect, so retaining older rows
1152
+ // cannot answer a future delivery and would grow without bound.
1153
+ tx.delete(schema.hostProjectEnvReplicaReceipts).run();
1154
+ }
1155
+ tx.delete(schema.hostProjectEnvReplicaStageValues).run();
1156
+ tx.delete(schema.hostProjectEnvReplicaStageRecords).run();
1157
+ tx.delete(schema.hostProjectEnvReplicaStageProjects).run();
1158
+ for (const projectId of frame.projectIds) {
1159
+ tx.insert(schema.hostProjectEnvReplicaStageProjects)
1160
+ .values({
1161
+ attemptId: frame.attempt.attemptId,
1162
+ projectId,
1163
+ recordVersion: null,
1164
+ lastKey: null,
1165
+ complete: false,
1166
+ })
1167
+ .run();
1168
+ resetStagedProject(tx, frame.attempt.attemptId, projectId);
1169
+ }
1170
+ tx.update(schema.hostProjectEnvReplicaAuthority)
1171
+ .set({
1172
+ activated: true,
1173
+ gateOpen: false,
1174
+ streamId: frame.attempt.streamId,
1175
+ appliedSequence: changedStream ? 0 : before.appliedSequence,
1176
+ attemptId: frame.attempt.attemptId,
1177
+ orgId,
1178
+ policyEpochsJson: JSON.stringify(keptPolicyEpochs),
1179
+ updatedAt: Date.now(),
1180
+ })
1181
+ .where(eq(schema.hostProjectEnvReplicaAuthority.id, "replica"))
1182
+ .run();
1183
+ return {
1184
+ appliedThrough: {
1185
+ streamId: frame.attempt.streamId,
1186
+ sequence: changedStream ? 0 : before.appliedSequence,
1187
+ },
1188
+ policyEpochs: [...keptPolicyEpochs],
1189
+ };
1190
+ });
1191
+ notifyProjects([...beforeProjects, ...frame.projectIds]);
1192
+ return result;
1193
+ }
1194
+
1195
+ /** Install one stable, canonical page into the closed staging view. */
1196
+ export async function applyProjectEnvReplicaPage(
1197
+ frame: ProjectEnvReplicaPageFrame,
1198
+ reader: ProjectEnvReplicaValueReader | null = defaultValueReader(),
1199
+ ): Promise<ProjectEnvReplicaPageResult> {
1200
+ if (!isProjectEnvReplicaPageFrame(frame)) {
1201
+ throw new Error("invalid project environment replica page");
1202
+ }
1203
+ type Prepared =
1204
+ | { done: ProjectEnvReplicaPageResult }
1205
+ | {
1206
+ done?: never;
1207
+ fetchKeys: string[];
1208
+ epochs: number[];
1209
+ appliedThrough: ProjectEnvDeliveryCursor;
1210
+ };
1211
+ const prepared: Prepared = getDb().transaction((tx): Prepared => {
1212
+ const authority = authorityRow(tx);
1213
+ const appliedThrough = requiredCursor(authority);
1214
+ const epochs = policyEpochs(authority);
1215
+ const refuse = (code: string): ProjectEnvReplicaPageResult => ({
1216
+ ok: false,
1217
+ attemptId: frame.attemptId,
1218
+ code,
1219
+ policyEpochs: epochs,
1220
+ appliedThrough,
1221
+ });
1222
+ if (authority.attemptId !== frame.attemptId || authority.gateOpen) {
1223
+ return { done: refuse("reconciliation_attempt_mismatch") } as const;
1224
+ }
1225
+ if (!pageFits(frame, frame.records, epochs, appliedThrough)) {
1226
+ return { done: refuse("ack_page_too_large") } as const;
1227
+ }
1228
+ const staged = tx
1229
+ .select()
1230
+ .from(schema.hostProjectEnvReplicaStageProjects)
1231
+ .where(
1232
+ and(
1233
+ eq(schema.hostProjectEnvReplicaStageProjects.attemptId, frame.attemptId),
1234
+ eq(schema.hostProjectEnvReplicaStageProjects.projectId, frame.projectId),
1235
+ ),
1236
+ )
1237
+ .get();
1238
+ if (!staged) return { done: refuse("project_not_in_attempt") } as const;
1239
+ const liveVersion =
1240
+ tx
1241
+ .select({ recordVersion: schema.hostProjectEnvReplicaProjects.recordVersion })
1242
+ .from(schema.hostProjectEnvReplicaProjects)
1243
+ .where(eq(schema.hostProjectEnvReplicaProjects.projectId, frame.projectId))
1244
+ .get()?.recordVersion ?? 0;
1245
+ if (frame.recordVersion < liveVersion) {
1246
+ return { done: refuse("record_version_stale") } as const;
1247
+ }
1248
+ if (frame.after === null) {
1249
+ resetStagedProject(tx, frame.attemptId, frame.projectId);
1250
+ tx.update(schema.hostProjectEnvReplicaStageProjects)
1251
+ .set({
1252
+ recordVersion: frame.recordVersion,
1253
+ lastKey: null,
1254
+ complete: false,
1255
+ })
1256
+ .where(
1257
+ and(
1258
+ eq(schema.hostProjectEnvReplicaStageProjects.attemptId, frame.attemptId),
1259
+ eq(schema.hostProjectEnvReplicaStageProjects.projectId, frame.projectId),
1260
+ ),
1261
+ )
1262
+ .run();
1263
+ } else if (
1264
+ staged.complete ||
1265
+ staged.recordVersion !== frame.recordVersion ||
1266
+ staged.lastKey !== frame.after
1267
+ ) {
1268
+ return { done: refuse("snapshot_page_mismatch") } as const;
1269
+ }
1270
+ const fetchKeys: string[] = [];
1271
+ const retainedStates: ProjectEnvReplicaBindingState[] = [];
1272
+ let sameVersionConflict = false;
1273
+ for (const state of frame.records) {
1274
+ if (frame.recordVersion === liveVersion) {
1275
+ const before = stageRecord(
1276
+ tx,
1277
+ frame.attemptId,
1278
+ frame.projectId,
1279
+ state.key,
1280
+ );
1281
+ if (
1282
+ before === null ||
1283
+ stateOrder(state, rowState(before)) !== "same"
1284
+ ) {
1285
+ sameVersionConflict = true;
1286
+ }
1287
+ }
1288
+ const retained = retainStageState(
1289
+ tx,
1290
+ frame.attemptId,
1291
+ frame.projectId,
1292
+ state,
1293
+ epochs,
1294
+ );
1295
+ retainedStates.push(retained.result.state);
1296
+ if (retained.fetch) fetchKeys.push(retained.result.state.key);
1297
+ }
1298
+ // A machine ahead of/conflicting with the page echoes what it retained.
1299
+ // That state can be wider than the incoming one, so only sizing the
1300
+ // request would permit an oversized acknowledgement.
1301
+ if (!pageFits(frame, retainedStates, epochs, appliedThrough)) {
1302
+ return { done: refuse("ack_page_too_large") };
1303
+ }
1304
+ if (sameVersionConflict) {
1305
+ return { done: refuse("snapshot_version_conflict") };
1306
+ }
1307
+ if (frame.complete && frame.recordVersion === liveVersion) {
1308
+ const liveRows = tx
1309
+ .select()
1310
+ .from(schema.hostProjectEnvReplicaRecords)
1311
+ .where(eq(schema.hostProjectEnvReplicaRecords.projectId, frame.projectId))
1312
+ .orderBy(asc(schema.hostProjectEnvReplicaRecords.key))
1313
+ .all();
1314
+ const stagedRows = tx
1315
+ .select()
1316
+ .from(schema.hostProjectEnvReplicaStageRecords)
1317
+ .where(
1318
+ and(
1319
+ eq(schema.hostProjectEnvReplicaStageRecords.attemptId, frame.attemptId),
1320
+ eq(schema.hostProjectEnvReplicaStageRecords.projectId, frame.projectId),
1321
+ ),
1322
+ )
1323
+ .orderBy(asc(schema.hostProjectEnvReplicaStageRecords.key))
1324
+ .all()
1325
+ .filter((row) => row.present);
1326
+ if (
1327
+ liveRows.length !== stagedRows.length ||
1328
+ liveRows.some(
1329
+ (row, index) =>
1330
+ stagedRows[index] === undefined ||
1331
+ !sameState(rowState(row), rowState(stagedRows[index]!)),
1332
+ )
1333
+ ) {
1334
+ return { done: refuse("snapshot_version_conflict") };
1335
+ }
1336
+ }
1337
+ tx.update(schema.hostProjectEnvReplicaStageProjects)
1338
+ .set({
1339
+ recordVersion: frame.recordVersion,
1340
+ lastKey: frame.records.at(-1)?.key ?? null,
1341
+ complete: frame.complete,
1342
+ })
1343
+ .where(
1344
+ and(
1345
+ eq(schema.hostProjectEnvReplicaStageProjects.attemptId, frame.attemptId),
1346
+ eq(schema.hostProjectEnvReplicaStageProjects.projectId, frame.projectId),
1347
+ ),
1348
+ )
1349
+ .run();
1350
+ return { fetchKeys, epochs, appliedThrough } as const;
1351
+ });
1352
+ if (prepared.done !== undefined) return prepared.done;
1353
+ for (const key of prepared.fetchKeys) {
1354
+ await materializeStage(frame.attemptId, frame.projectId, key, reader);
1355
+ }
1356
+ return getDb().transaction((tx) => {
1357
+ const authority = authorityRow(tx);
1358
+ const appliedThrough = requiredCursor(authority);
1359
+ const epochs = policyEpochs(authority);
1360
+ if (authority.attemptId !== frame.attemptId || authority.gateOpen) {
1361
+ return {
1362
+ ok: false,
1363
+ attemptId: frame.attemptId,
1364
+ code: "reconciliation_attempt_mismatch",
1365
+ policyEpochs: epochs,
1366
+ appliedThrough,
1367
+ };
1368
+ }
1369
+ const results = frame.records.map((state) => {
1370
+ const row = stageRecord(tx, frame.attemptId, frame.projectId, state.key);
1371
+ if (row === null) {
1372
+ throw new ProjectEnvReplicaCorruptError(
1373
+ "staged replica page record disappeared",
1374
+ );
1375
+ }
1376
+ return rowResult(row);
1377
+ });
1378
+ return {
1379
+ ok: true,
1380
+ attemptId: frame.attemptId,
1381
+ projectId: frame.projectId,
1382
+ after: frame.after,
1383
+ recordVersion: frame.recordVersion,
1384
+ results,
1385
+ complete: frame.complete,
1386
+ policyEpochs: epochs,
1387
+ appliedThrough,
1388
+ };
1389
+ });
1390
+ }
1391
+
1392
+ function receiptResult(
1393
+ row: ReplicaReceiptRow,
1394
+ authority: ReplicaAuthorityRow,
1395
+ projectId: string,
1396
+ ): ProjectEnvReplicaApplyResult {
1397
+ const appliedThrough = requiredCursor(authority);
1398
+ const epochs = policyEpochs(authority);
1399
+ if (!row.ok) {
1400
+ if (row.code === null) {
1401
+ throw new ProjectEnvReplicaCorruptError("replica refusal has no code");
1402
+ }
1403
+ return {
1404
+ ok: false,
1405
+ replayed: true,
1406
+ consumed: true,
1407
+ attemptId: authority.attemptId,
1408
+ code: row.code,
1409
+ policyEpochs: epochs,
1410
+ appliedThrough,
1411
+ };
1412
+ }
1413
+ const parsed = parseJson(row.resultJson, "replica delivery result");
1414
+ if (!isProjectEnvReplicaResult(parsed) || row.recordVersion === null) {
1415
+ throw new ProjectEnvReplicaCorruptError(
1416
+ "stored replica delivery result is invalid",
1417
+ );
1418
+ }
1419
+ return {
1420
+ ok: true,
1421
+ replayed: true,
1422
+ attemptId: authority.attemptId,
1423
+ projectId,
1424
+ recordVersion: row.recordVersion,
1425
+ result: parsed,
1426
+ policyEpochs: epochs,
1427
+ appliedThrough,
1428
+ };
1429
+ }
1430
+
1431
+ function saveReceipt(
1432
+ tx: ReplicaTransaction,
1433
+ frame: ProjectEnvReplicaApplyFrame,
1434
+ outcome:
1435
+ | { ok: true; result: ProjectEnvReplicaResult; recordVersion: number }
1436
+ | { ok: false; code: string },
1437
+ ): void {
1438
+ tx.insert(schema.hostProjectEnvReplicaReceipts)
1439
+ .values({
1440
+ streamId: frame.delivery.streamId,
1441
+ sequence: frame.delivery.sequence,
1442
+ requestMac: requestMac(frame),
1443
+ resultJson: outcome.ok ? JSON.stringify(outcome.result) : null,
1444
+ recordVersion: outcome.ok ? outcome.recordVersion : null,
1445
+ ok: outcome.ok,
1446
+ code: outcome.ok ? null : outcome.code,
1447
+ createdAt: Date.now(),
1448
+ })
1449
+ .run();
1450
+ tx.update(schema.hostProjectEnvReplicaAuthority)
1451
+ .set({ appliedSequence: frame.delivery.sequence, updatedAt: Date.now() })
1452
+ .where(eq(schema.hostProjectEnvReplicaAuthority.id, "replica"))
1453
+ .run();
1454
+ }
1455
+
1456
+ function transientApply(
1457
+ authority: ReplicaAuthorityRow,
1458
+ code: string,
1459
+ ): ProjectEnvReplicaApplyResult {
1460
+ return {
1461
+ ok: false,
1462
+ replayed: false,
1463
+ consumed: false,
1464
+ attemptId: authority.attemptId,
1465
+ code,
1466
+ policyEpochs: policyEpochs(authority),
1467
+ appliedThrough: requiredCursor(authority),
1468
+ };
1469
+ }
1470
+
1471
+ /** Apply/replay exactly one ordered cloud-authoritative delta. */
1472
+ export async function applyProjectEnvReplicaDelivery(
1473
+ frame: ProjectEnvReplicaApplyFrame,
1474
+ reader: ProjectEnvReplicaValueReader | null = defaultValueReader(),
1475
+ ): Promise<ProjectEnvReplicaApplyResult> {
1476
+ if (!isProjectEnvReplicaApplyFrame(frame)) {
1477
+ throw new Error("invalid project environment replica delivery");
1478
+ }
1479
+ type Prepared =
1480
+ | { done: ProjectEnvReplicaApplyResult }
1481
+ | {
1482
+ done?: never;
1483
+ retained: {
1484
+ result: ProjectEnvReplicaResult;
1485
+ fetch: boolean;
1486
+ changed: boolean;
1487
+ };
1488
+ actualVersion: number;
1489
+ attemptId: string | null;
1490
+ changedLive: boolean;
1491
+ };
1492
+ const prepared: Prepared = getDb().transaction((tx): Prepared => {
1493
+ const authority = authorityRow(tx);
1494
+ if (!authority.activated) {
1495
+ throw new ProjectEnvReplicaCorruptError(
1496
+ "replica delivery arrived before activation",
1497
+ );
1498
+ }
1499
+ const current = requiredCursor(authority);
1500
+ if (frame.delivery.streamId !== current.streamId) {
1501
+ return { done: transientApply(authority, "stream_mismatch") } as const;
1502
+ }
1503
+ if (frame.delivery.sequence <= current.sequence) {
1504
+ const receipt = tx
1505
+ .select()
1506
+ .from(schema.hostProjectEnvReplicaReceipts)
1507
+ .where(
1508
+ and(
1509
+ eq(schema.hostProjectEnvReplicaReceipts.streamId, frame.delivery.streamId),
1510
+ eq(schema.hostProjectEnvReplicaReceipts.sequence, frame.delivery.sequence),
1511
+ ),
1512
+ )
1513
+ .get();
1514
+ if (!receipt) return { done: transientApply(authority, "receipt_missing") } as const;
1515
+ if (!sameMac(receipt.requestMac, requestMac(frame))) {
1516
+ return {
1517
+ done: {
1518
+ ok: false,
1519
+ replayed: true,
1520
+ consumed: true,
1521
+ attemptId: authority.attemptId,
1522
+ code: "delivery_conflict",
1523
+ policyEpochs: policyEpochs(authority),
1524
+ appliedThrough: current,
1525
+ },
1526
+ } as const;
1527
+ }
1528
+ const replay = receiptResult(receipt, authority, frame.projectId);
1529
+ return { done: replay } as const;
1530
+ }
1531
+ if (frame.delivery.sequence !== current.sequence + 1) {
1532
+ return { done: transientApply(authority, "delivery_gap") } as const;
1533
+ }
1534
+ if (
1535
+ authority.gateOpen
1536
+ ? frame.reconciliationAttemptId !== undefined
1537
+ : authority.attemptId === null ||
1538
+ frame.reconciliationAttemptId !== authority.attemptId
1539
+ ) {
1540
+ return {
1541
+ done: transientApply(authority, "reconciliation_attempt_mismatch"),
1542
+ } as const;
1543
+ }
1544
+ const epochs = policyEpochs(authority);
1545
+ const consumed = (code: string): Prepared => {
1546
+ saveReceipt(tx, frame, { ok: false, code });
1547
+ const after = authorityRow(tx);
1548
+ return {
1549
+ done: {
1550
+ ok: false,
1551
+ replayed: false,
1552
+ consumed: true,
1553
+ attemptId: after.attemptId,
1554
+ code,
1555
+ policyEpochs: epochs,
1556
+ appliedThrough: requiredCursor(after),
1557
+ },
1558
+ };
1559
+ };
1560
+ let retained:
1561
+ | { result: ProjectEnvReplicaResult; fetch: boolean; changed: boolean }
1562
+ | null = null;
1563
+ let actualVersion: number;
1564
+ if (authority.attemptId !== null) {
1565
+ const staged = tx
1566
+ .select()
1567
+ .from(schema.hostProjectEnvReplicaStageProjects)
1568
+ .where(
1569
+ and(
1570
+ eq(schema.hostProjectEnvReplicaStageProjects.attemptId, authority.attemptId),
1571
+ eq(schema.hostProjectEnvReplicaStageProjects.projectId, frame.projectId),
1572
+ ),
1573
+ )
1574
+ .get();
1575
+ if (!staged || !staged.complete || staged.recordVersion === null) {
1576
+ return { done: transientApply(authority, "snapshot_incomplete") } as const;
1577
+ }
1578
+ if (frame.recordVersion > staged.recordVersion + 1) {
1579
+ return { done: transientApply(authority, "record_version_gap") } as const;
1580
+ }
1581
+ const stored = stageRecord(
1582
+ tx,
1583
+ authority.attemptId,
1584
+ frame.projectId,
1585
+ frame.state.key,
1586
+ );
1587
+ const order =
1588
+ stored === null ? "newer" : stateOrder(frame.state, rowState(stored));
1589
+ const advances =
1590
+ frame.recordVersion === staged.recordVersion + 1 &&
1591
+ order === "newer" &&
1592
+ isReachableRecordAdvance(
1593
+ frame.state,
1594
+ stored === null ? null : rowState(stored),
1595
+ );
1596
+ if (advances) {
1597
+ const stagedResult = retainStageState(
1598
+ tx,
1599
+ authority.attemptId,
1600
+ frame.projectId,
1601
+ frame.state,
1602
+ epochs,
1603
+ );
1604
+ retained = { ...stagedResult, changed: false };
1605
+ actualVersion = frame.recordVersion;
1606
+ tx.update(schema.hostProjectEnvReplicaStageProjects)
1607
+ .set({ recordVersion: actualVersion })
1608
+ .where(
1609
+ and(
1610
+ eq(schema.hostProjectEnvReplicaStageProjects.attemptId, authority.attemptId),
1611
+ eq(schema.hostProjectEnvReplicaStageProjects.projectId, frame.projectId),
1612
+ ),
1613
+ )
1614
+ .run();
1615
+ } else if (stored !== null) {
1616
+ // A lower/equal version, an older state, or an invalid transition is
1617
+ // answered with what this replica actually retained. The cloud can
1618
+ // then quarantine/reconcile without the stream getting stuck.
1619
+ retained = retainedExisting(stored, epochs);
1620
+ actualVersion = staged.recordVersion;
1621
+ } else {
1622
+ return consumed("record_version_conflict");
1623
+ }
1624
+ } else {
1625
+ const project = tx
1626
+ .select()
1627
+ .from(schema.hostProjectEnvReplicaProjects)
1628
+ .where(eq(schema.hostProjectEnvReplicaProjects.projectId, frame.projectId))
1629
+ .get();
1630
+ if (!project) {
1631
+ return consumed("project_not_in_replica");
1632
+ }
1633
+ if (frame.recordVersion > project.recordVersion + 1) {
1634
+ return { done: transientApply(authority, "record_version_gap") } as const;
1635
+ }
1636
+ const stored = liveRecord(tx, frame.projectId, frame.state.key);
1637
+ const order =
1638
+ stored === null ? "newer" : stateOrder(frame.state, rowState(stored));
1639
+ const advances =
1640
+ frame.recordVersion === project.recordVersion + 1 &&
1641
+ order === "newer" &&
1642
+ isReachableRecordAdvance(
1643
+ frame.state,
1644
+ stored === null ? null : rowState(stored),
1645
+ );
1646
+ if (advances) {
1647
+ retained = retainLiveState(tx, frame.projectId, frame.state, epochs);
1648
+ actualVersion = frame.recordVersion;
1649
+ tx.update(schema.hostProjectEnvReplicaProjects)
1650
+ .set({ recordVersion: actualVersion })
1651
+ .where(eq(schema.hostProjectEnvReplicaProjects.projectId, frame.projectId))
1652
+ .run();
1653
+ } else if (stored !== null) {
1654
+ retained = retainedExisting(stored, epochs);
1655
+ actualVersion = project.recordVersion;
1656
+ } else {
1657
+ return consumed("record_version_conflict");
1658
+ }
1659
+ }
1660
+ return {
1661
+ retained,
1662
+ actualVersion,
1663
+ attemptId: authority.attemptId,
1664
+ changedLive: authority.attemptId === null && retained.changed,
1665
+ } as const;
1666
+ });
1667
+ if (prepared.done !== undefined) return prepared.done;
1668
+ if (prepared.changedLive) noteProjectEnvValueMutation(frame.projectId);
1669
+ if (prepared.retained.fetch) {
1670
+ if (prepared.attemptId === null) {
1671
+ await materializeLive(frame.projectId, frame.state.key, reader);
1672
+ } else {
1673
+ await materializeStage(
1674
+ prepared.attemptId,
1675
+ frame.projectId,
1676
+ frame.state.key,
1677
+ reader,
1678
+ );
1679
+ }
1680
+ }
1681
+ return getDb().transaction((tx) => {
1682
+ const authority = authorityRow(tx);
1683
+ const current = requiredCursor(authority);
1684
+ if (
1685
+ frame.delivery.streamId !== current.streamId ||
1686
+ frame.delivery.sequence !== current.sequence + 1 ||
1687
+ authority.attemptId !== prepared.attemptId
1688
+ ) {
1689
+ return transientApply(authority, "replica_state_changed");
1690
+ }
1691
+ const row =
1692
+ prepared.attemptId === null
1693
+ ? liveRecord(tx, frame.projectId, frame.state.key)
1694
+ : stageRecord(
1695
+ tx,
1696
+ prepared.attemptId,
1697
+ frame.projectId,
1698
+ frame.state.key,
1699
+ );
1700
+ if (row === null) {
1701
+ return transientApply(authority, "replica_state_missing");
1702
+ }
1703
+ const result = rowResult(row);
1704
+ saveReceipt(tx, frame, {
1705
+ ok: true,
1706
+ result,
1707
+ recordVersion: prepared.actualVersion,
1708
+ });
1709
+ const after = authorityRow(tx);
1710
+ return {
1711
+ ok: true,
1712
+ replayed: false,
1713
+ attemptId: after.attemptId,
1714
+ projectId: frame.projectId,
1715
+ recordVersion: prepared.actualVersion,
1716
+ result,
1717
+ policyEpochs: policyEpochs(after),
1718
+ appliedThrough: requiredCursor(after),
1719
+ };
1720
+ });
1721
+ }
1722
+
1723
+ /** Promote the exact staged task view and open the serving gate atomically. */
1724
+ export function completeProjectEnvReplica(
1725
+ frame: ProjectEnvReplicaCommitFrame,
1726
+ ): ProjectEnvReplicaCommitResult {
1727
+ if (!isProjectEnvReplicaCommitFrame(frame)) {
1728
+ throw new Error("invalid project environment replica commit");
1729
+ }
1730
+ const beforeProjects = sortedProjectIds(getDb());
1731
+ const result: ProjectEnvReplicaCommitResult = getDb().transaction(
1732
+ (tx): ProjectEnvReplicaCommitResult => {
1733
+ const authority = authorityRow(tx);
1734
+ const appliedThrough = requiredCursor(authority);
1735
+ const epochs = policyEpochs(authority);
1736
+ const refuse = (code: string): ProjectEnvReplicaCommitResult => ({
1737
+ ok: false,
1738
+ attemptId: frame.attemptId,
1739
+ code,
1740
+ policyEpochs: epochs,
1741
+ appliedThrough,
1742
+ });
1743
+ if (authority.attemptId !== frame.attemptId || authority.gateOpen) {
1744
+ return refuse("reconciliation_attempt_mismatch");
1745
+ }
1746
+ if (
1747
+ frame.highWater.streamId !== appliedThrough.streamId ||
1748
+ frame.highWater.sequence !== appliedThrough.sequence
1749
+ ) {
1750
+ return refuse("delivery_incomplete");
1751
+ }
1752
+ const stagedProjects = tx
1753
+ .select()
1754
+ .from(schema.hostProjectEnvReplicaStageProjects)
1755
+ .where(eq(schema.hostProjectEnvReplicaStageProjects.attemptId, frame.attemptId))
1756
+ .orderBy(asc(schema.hostProjectEnvReplicaStageProjects.projectId))
1757
+ .all();
1758
+ if (
1759
+ stagedProjects.length !== frame.versions.length ||
1760
+ stagedProjects.some(
1761
+ (project, index) =>
1762
+ !project.complete ||
1763
+ project.recordVersion === null ||
1764
+ project.projectId !== frame.versions[index]?.projectId ||
1765
+ project.recordVersion !== frame.versions[index]?.recordVersion,
1766
+ )
1767
+ ) {
1768
+ return refuse("record_version_changed");
1769
+ }
1770
+
1771
+ const records = tx
1772
+ .select()
1773
+ .from(schema.hostProjectEnvReplicaStageRecords)
1774
+ .where(eq(schema.hostProjectEnvReplicaStageRecords.attemptId, frame.attemptId))
1775
+ .orderBy(
1776
+ asc(schema.hostProjectEnvReplicaStageRecords.projectId),
1777
+ asc(schema.hostProjectEnvReplicaStageRecords.key),
1778
+ )
1779
+ .all()
1780
+ .filter((row) => row.present);
1781
+ const liveKeys = new Set(records.map((row) => `${row.projectId}\0${row.key}`));
1782
+ const values = tx
1783
+ .select()
1784
+ .from(schema.hostProjectEnvReplicaStageValues)
1785
+ .where(eq(schema.hostProjectEnvReplicaStageValues.attemptId, frame.attemptId))
1786
+ .all();
1787
+ const valuesByKey = new Map(
1788
+ values.map((row) => [`${row.projectId}\0${row.key}`, row]),
1789
+ );
1790
+ for (const row of records) {
1791
+ const state = rowState(row);
1792
+ const materialization = rowMaterialization(row);
1793
+ const value = valuesByKey.get(`${row.projectId}\0${row.key}`);
1794
+ if (materialization.kind === "installed") {
1795
+ if (
1796
+ value === undefined ||
1797
+ value.revision !== state.head.revision ||
1798
+ value.generation !== state.head.generation ||
1799
+ value.valueRefJson !== JSON.stringify(state.head.valueRef)
1800
+ ) {
1801
+ throw new ProjectEnvReplicaCorruptError(
1802
+ "staged installed value does not match its replica head",
1803
+ );
1804
+ }
1805
+ } else if (value !== undefined) {
1806
+ throw new ProjectEnvReplicaCorruptError(
1807
+ "staged withheld replica record still has a value",
1808
+ );
1809
+ }
1810
+ }
1811
+
1812
+ tx.delete(schema.hostProjectEnvReplicaValues).run();
1813
+ tx.delete(schema.hostProjectEnvReplicaRecords).run();
1814
+ tx.delete(schema.hostProjectEnvReplicaProjects).run();
1815
+ for (const project of stagedProjects) {
1816
+ tx.insert(schema.hostProjectEnvReplicaProjects)
1817
+ .values({
1818
+ projectId: project.projectId,
1819
+ recordVersion: project.recordVersion!,
1820
+ })
1821
+ .run();
1822
+ }
1823
+ for (const row of records) {
1824
+ putLiveRecord(tx, row.projectId, rowState(row), rowMaterialization(row));
1825
+ }
1826
+ for (const row of values) {
1827
+ if (!liveKeys.has(`${row.projectId}\0${row.key}`)) continue;
1828
+ tx.insert(schema.hostProjectEnvReplicaValues)
1829
+ .values({
1830
+ projectId: row.projectId,
1831
+ key: row.key,
1832
+ revision: row.revision,
1833
+ generation: row.generation,
1834
+ valueRefJson: row.valueRefJson,
1835
+ valueEnc: row.valueEnc,
1836
+ updatedAt: Date.now(),
1837
+ })
1838
+ .run();
1839
+ }
1840
+ tx.delete(schema.hostProjectEnvReplicaStageValues).run();
1841
+ tx.delete(schema.hostProjectEnvReplicaStageRecords).run();
1842
+ tx.delete(schema.hostProjectEnvReplicaStageProjects).run();
1843
+ tx.update(schema.hostProjectEnvReplicaAuthority)
1844
+ .set({ gateOpen: true, attemptId: null, updatedAt: Date.now() })
1845
+ .where(eq(schema.hostProjectEnvReplicaAuthority.id, "replica"))
1846
+ .run();
1847
+ return {
1848
+ ok: true,
1849
+ attemptId: frame.attemptId,
1850
+ policyEpochs: epochs,
1851
+ appliedThrough,
1852
+ };
1853
+ },
1854
+ );
1855
+ if (result.ok) {
1856
+ notifyProjects([...beforeProjects, ...frame.versions.map((version) => version.projectId)]);
1857
+ }
1858
+ return result;
1859
+ }
1860
+
1861
+ /** Each sidecar connection/restart closes the replica before any task read. */
1862
+ export function closeProjectEnvReplicaGate(): void {
1863
+ const projects = sortedProjectIds(getDb());
1864
+ getDb().transaction((tx) => {
1865
+ // A snapshot attempt belongs to one authenticated sidecar connection.
1866
+ // Purge it when that connection closes so a replacement cannot resume or
1867
+ // commit work prepared under the old connection.
1868
+ tx.delete(schema.hostProjectEnvReplicaStageValues).run();
1869
+ tx.delete(schema.hostProjectEnvReplicaStageRecords).run();
1870
+ tx.delete(schema.hostProjectEnvReplicaStageProjects).run();
1871
+ tx.update(schema.hostProjectEnvReplicaAuthority)
1872
+ .set({ gateOpen: false, attemptId: null, updatedAt: Date.now() })
1873
+ .where(eq(schema.hostProjectEnvReplicaAuthority.id, "replica"))
1874
+ .run();
1875
+ });
1876
+ notifyProjects(projects);
1877
+ }
1878
+
1879
+ export function readProjectEnvReplicaStatus(): ProjectEnvReplicaStatus {
1880
+ return getDb().transaction((tx) => {
1881
+ const row = authorityRow(tx);
1882
+ return {
1883
+ activated: row.activated,
1884
+ gateOpen: row.gateOpen,
1885
+ attemptId: row.attemptId,
1886
+ orgId: row.orgId,
1887
+ appliedThrough: cursor(row),
1888
+ policyEpochs: policyEpochs(row),
1889
+ projectIds: sortedProjectIds(tx),
1890
+ };
1891
+ });
1892
+ }
1893
+
1894
+ /** How long a bindings-capable sidecar TaskUp waits for its first complete
1895
+ * replica reconciliation before returning a retryable admission failure. */
1896
+ export const PROJECT_ENV_REPLICA_READY_TIMEOUT_MS = 3 * 60_000;
1897
+
1898
+ /**
1899
+ * Wait until the local replica is an activated, committed serving view.
1900
+ *
1901
+ * Subscribe before the second read so a begin/commit between the first read
1902
+ * and listener installation cannot be lost. Gate changes share the task-env
1903
+ * invalidation signal, avoiding a polling window and keeping an empty task
1904
+ * correct too.
1905
+ */
1906
+ export function awaitProjectEnvReplicaReady(
1907
+ timeoutMs = PROJECT_ENV_REPLICA_READY_TIMEOUT_MS,
1908
+ ): Promise<void> {
1909
+ const ready = (): boolean => {
1910
+ const status = readProjectEnvReplicaStatus();
1911
+ return status.activated && status.gateOpen && status.attemptId === null;
1912
+ };
1913
+ if (ready()) return Promise.resolve();
1914
+
1915
+ return new Promise<void>((resolve, reject) => {
1916
+ let settled = false;
1917
+ let timer: NodeJS.Timeout | null = null;
1918
+ let unsubscribe = (): void => {};
1919
+ const finish = (error?: unknown): void => {
1920
+ if (settled) return;
1921
+ settled = true;
1922
+ if (timer !== null) clearTimeout(timer);
1923
+ unsubscribe();
1924
+ if (error === undefined) resolve();
1925
+ else reject(error);
1926
+ };
1927
+ const check = (): void => {
1928
+ try {
1929
+ if (ready()) finish();
1930
+ } catch (error) {
1931
+ finish(error);
1932
+ }
1933
+ };
1934
+
1935
+ unsubscribe = onProjectEnvChange(check);
1936
+ timer = setTimeout(
1937
+ () =>
1938
+ finish(
1939
+ new Error(
1940
+ "task environment replica did not become ready before the admission deadline",
1941
+ ),
1942
+ ),
1943
+ timeoutMs,
1944
+ );
1945
+ timer.unref?.();
1946
+ check();
1947
+ });
1948
+ }
1949
+
1950
+ /**
1951
+ * Fail-closed live read for the task loader and proxy. The caller must name
1952
+ * the task's exact project set. Withheld/unavailable records remain visible
1953
+ * as metadata but carry no value; any mismatch or corrupt row rejects the
1954
+ * whole snapshot.
1955
+ */
1956
+ export function readProjectEnvReplicaSnapshot(
1957
+ projectIds: readonly string[],
1958
+ ): ProjectEnvReplicaSnapshot | null {
1959
+ try {
1960
+ return getDb().transaction((tx) => {
1961
+ const authority = authorityRow(tx);
1962
+ if (!authority.activated || !authority.gateOpen || authority.attemptId !== null) {
1963
+ return null;
1964
+ }
1965
+ const requested = [...projectIds];
1966
+ const expected = [...new Set(requested)].sort();
1967
+ if (
1968
+ expected.length !== requested.length ||
1969
+ !sameStrings(expected, sortedProjectIds(tx))
1970
+ ) {
1971
+ return null;
1972
+ }
1973
+ const projects: ProjectEnvReplicaSnapshotProject[] = [];
1974
+ // Preserve task order: the loader resolves duplicate key names with the
1975
+ // lead project first. Only set comparison is canonicalised above.
1976
+ for (const projectId of requested) {
1977
+ const project = tx
1978
+ .select()
1979
+ .from(schema.hostProjectEnvReplicaProjects)
1980
+ .where(eq(schema.hostProjectEnvReplicaProjects.projectId, projectId))
1981
+ .get();
1982
+ if (!project) throw new ProjectEnvReplicaCorruptError("replica project is missing");
1983
+ const rows = tx
1984
+ .select()
1985
+ .from(schema.hostProjectEnvReplicaRecords)
1986
+ .where(eq(schema.hostProjectEnvReplicaRecords.projectId, projectId))
1987
+ .orderBy(asc(schema.hostProjectEnvReplicaRecords.key))
1988
+ .all();
1989
+ const values = new Map(
1990
+ tx
1991
+ .select()
1992
+ .from(schema.hostProjectEnvReplicaValues)
1993
+ .where(eq(schema.hostProjectEnvReplicaValues.projectId, projectId))
1994
+ .all()
1995
+ .map((row) => [row.key, row]),
1996
+ );
1997
+ const entries: ProjectEnvReplicaSnapshotEntry[] = [];
1998
+ for (const row of rows) {
1999
+ const state = rowState(row);
2000
+ const materialization = rowMaterialization(row);
2001
+ const valueRow = values.get(row.key);
2002
+ let value: string | null = null;
2003
+ if (materialization.kind === "installed") {
2004
+ if (
2005
+ state.head.tombstone ||
2006
+ valueRow === undefined ||
2007
+ valueRow.revision !== state.head.revision ||
2008
+ valueRow.generation !== state.head.generation ||
2009
+ valueRow.valueRefJson !== JSON.stringify(state.head.valueRef)
2010
+ ) {
2011
+ throw new ProjectEnvReplicaCorruptError(
2012
+ "installed replica value does not match its head",
2013
+ );
2014
+ }
2015
+ value = openProjectEnvValueFromStorage(valueRow.valueEnc);
2016
+ values.delete(row.key);
2017
+ } else if (valueRow !== undefined) {
2018
+ throw new ProjectEnvReplicaCorruptError(
2019
+ "withheld replica record still has a value",
2020
+ );
2021
+ }
2022
+ entries.push({ state, materialization, value });
2023
+ }
2024
+ if (values.size > 0) {
2025
+ throw new ProjectEnvReplicaCorruptError(
2026
+ "replica value has no authority record",
2027
+ );
2028
+ }
2029
+ projects.push({
2030
+ projectId,
2031
+ recordVersion: project.recordVersion,
2032
+ entries,
2033
+ });
2034
+ }
2035
+ return { projects };
2036
+ });
2037
+ } catch {
2038
+ return null;
2039
+ }
2040
+ }
2041
+
2042
+ /** Diagnostics for tests and recovery; contains no values. */
2043
+ export function listProjectEnvReplicaReceipts(): Array<{
2044
+ streamId: string;
2045
+ sequence: number;
2046
+ requestMac: string;
2047
+ ok: boolean;
2048
+ code: string | null;
2049
+ result: ProjectEnvReplicaResult | null;
2050
+ recordVersion: number | null;
2051
+ }> {
2052
+ return getDb()
2053
+ .select()
2054
+ .from(schema.hostProjectEnvReplicaReceipts)
2055
+ .orderBy(
2056
+ asc(schema.hostProjectEnvReplicaReceipts.streamId),
2057
+ asc(schema.hostProjectEnvReplicaReceipts.sequence),
2058
+ )
2059
+ .all()
2060
+ .map((row) => {
2061
+ const result = parseJson(row.resultJson, "replica delivery result");
2062
+ if (result !== null && !isProjectEnvReplicaResult(result)) {
2063
+ throw new ProjectEnvReplicaCorruptError(
2064
+ "stored replica receipt result is invalid",
2065
+ );
2066
+ }
2067
+ return {
2068
+ streamId: row.streamId,
2069
+ sequence: row.sequence,
2070
+ requestMac: row.requestMac,
2071
+ ok: row.ok,
2072
+ code: row.code,
2073
+ result,
2074
+ recordVersion: row.recordVersion,
2075
+ };
2076
+ });
2077
+ }
2078
+
2079
+ /** Production adapter for callers that inject only the configured store. */
2080
+ export function projectEnvReplicaValueReader(
2081
+ store: CredentialStore | null = configuredCredentialStore(),
2082
+ ): ProjectEnvReplicaValueReader | null {
2083
+ if (store === null) return null;
2084
+ return {
2085
+ readStaged: (name) => store.get(name),
2086
+ readLegacy: async (name, version) => {
2087
+ if (store.getVersion === undefined) return null;
2088
+ const found = await store.getVersion(name, version);
2089
+ return found === null
2090
+ ? null
2091
+ : { value: found.value, version: found.version };
2092
+ },
2093
+ };
2094
+ }