@straiffi/archon 1.8.21 → 1.8.22

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 (131) hide show
  1. package/dist/client/assets/{ActivityView-BcguXTZ5.js → ActivityView-CdEPjmqD.js} +1 -1
  2. package/dist/client/assets/GitHubActionsDialog-CA189pr2.js +1 -0
  3. package/dist/client/assets/VerificationDialog-BPVkhKRj.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DnC7Twj0.js → architectureDiagram-3BPJPVTR-BrZ08mqq.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-D0VbjiKd.js → blockDiagram-GPEHLZMM-b93XwJ6Z.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-CzQ3l15K.js → c4Diagram-AAUBKEIU-BKOdbZ_v.js} +1 -1
  7. package/dist/client/assets/channel-D0zqzBT_.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-B5UGiqLW.js → chunk-2J33WTMH-D7Vxn_1m.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-SCksTqtu.js → chunk-3OPIFGDE-B1zo2UZq.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-IOkwLOtG.js → chunk-5ZQYHXKU-BajvkDdQ.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-Do60Xs9p.js → chunk-727SXJPM-CSH7cVpV.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CuQT3imB.js → chunk-AQP2D5EJ-DE-wgUNP.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-BkxMIESX.js → chunk-CSCIHK7Q-Ba7JgCEf.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-uUU3sWgR.js → chunk-KSCS5N6A-0o3YWkHM.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DLYccV6J.js → chunk-L5ZTLDWV-DihWczP8.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-CgJFfCRZ.js → chunk-LZXEDZCA-Bxnu-hme.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-D7IQSQfK.js → chunk-ND2GUHAM-CoaR5kmP.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B4O6JWAo.js → chunk-NZK2D7GU-By92Rd3d.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DidlQEqb.js → chunk-O5CBEL6O-D83Pkgfu.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-2-oq_Gue.js → chunk-WU5MYG2G-v8OVf5Lz.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-ChhyvHH2.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Cl02kurK.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-TubuN0hR.js → dagre-BM42HDAG-Dq1ALF7Z.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DNcUk6rV.js → diagram-2AECGRRQ-1t1_TKtu.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-B1sJn4w_.js → diagram-5GNKFQAL-DtvYRL6f.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-CnW6ZuQe.js → diagram-KO2AKTUF-PRNrSX5F.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-YHUmDzyS.js → diagram-LMA3HP47-DaTX0DhT.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-BkOQphxq.js → diagram-OG6HWLK6-C2p9p-92.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-oNMxAdxN.js → erDiagram-TEJ5UH35-DKzLtnp-.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-ByuCV-Am.js → flowDiagram-I6XJVG4X-GdL8lm6e.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRkNH8Ft.js → ganttDiagram-6RSMTGT7-Ok-4XYwT.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DRXzXjd8.js → gitGraphDiagram-PVQCEYII-DSMrOCK7.js} +1 -1
  33. package/dist/client/assets/index-Bs8KsdV9.js +276 -0
  34. package/dist/client/assets/index-DdwM8Wgt.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CJ4-hWch.js → infoDiagram-5YYISTIA-Dl4hRXis.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CW1xqvim.js → ishikawaDiagram-YF4QCWOH-C1Dc1fTT.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BVVFg8_h.js → journeyDiagram-JHISSGLW-CUscmJze.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-AcLZ63tA.js → kanban-definition-UN3LZRKU-C0nwYjQV.js} +1 -1
  39. package/dist/client/assets/{line-CG_TG6Ci.js → line-ByiuEIg_.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-UDLSm_c3.js → mermaid-parser.core-B-XdAyDR.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-IvBk22DF.js → mermaid.core-D_bIfuZD.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BflWNtnl.js → mindmap-definition-RKZ34NQL-BGNiPJDK.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-Dm1iGMm7.js → pieDiagram-4H26LBE5-CP6FpAoO.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-CM4zJd_R.js → quadrantDiagram-W4KKPZXB-dsAWBAQn.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-miuP86cp.js → requirementDiagram-4Y6WPE33-DJH-zHqF.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-zUcRagtr.js → sankeyDiagram-5OEKKPKP-BJMnZrqH.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BRYqVpsT.js → sequenceDiagram-3UESZ5HK-CzNjUSlM.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DwcF0DJm.js → stateDiagram-AJRCARHV-jppMhOwh.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CLR1WgdV.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BzH929s_.js → timeline-definition-PNZ67QCA-BX2CI5H_.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BIq8rooy.js → vennDiagram-CIIHVFJN-Bd1KZaXh.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CyV_gL5J.js → wardleyDiagram-YWT4CUSO-D-0BNz49.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Dxo-U-qe.js → xychartDiagram-2RQKCTM6-DbYa0ZWK.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +353 -19
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +121 -3
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/approvalReconciliation.js +101 -0
  60. package/dist/server/lib/approvalReconciliation.js.map +1 -0
  61. package/dist/server/lib/approvalRequests.js +587 -0
  62. package/dist/server/lib/approvalRequests.js.map +1 -0
  63. package/dist/server/lib/approvalRoutes.js +143 -0
  64. package/dist/server/lib/approvalRoutes.js.map +1 -0
  65. package/dist/server/lib/buildExecutionEnvironment.js +128 -11
  66. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  67. package/dist/server/lib/checkDefinitions.js +29 -1
  68. package/dist/server/lib/checkDefinitions.js.map +1 -1
  69. package/dist/server/lib/checkRunner.js +306 -27
  70. package/dist/server/lib/checkRunner.js.map +1 -1
  71. package/dist/server/lib/checkRuns.js +4 -2
  72. package/dist/server/lib/checkRuns.js.map +1 -1
  73. package/dist/server/lib/consequentialActionAuthority.js +142 -0
  74. package/dist/server/lib/consequentialActionAuthority.js.map +1 -0
  75. package/dist/server/lib/consequentialActions.js +231 -0
  76. package/dist/server/lib/consequentialActions.js.map +1 -0
  77. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +119 -0
  78. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  79. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +174 -11
  80. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  81. package/dist/server/lib/dockerSandboxExecutionProvider.js +1 -1
  82. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  83. package/dist/server/lib/dockerSandboxNetworkPolicy.js +222 -0
  84. package/dist/server/lib/dockerSandboxNetworkPolicy.js.map +1 -0
  85. package/dist/server/lib/executionEnvironmentContexts.js +4 -2
  86. package/dist/server/lib/executionEnvironmentContexts.js.map +1 -1
  87. package/dist/server/lib/executionEnvironmentLifecycle.js +34 -0
  88. package/dist/server/lib/executionEnvironmentLifecycle.js.map +1 -1
  89. package/dist/server/lib/executionEnvironmentRoutes.js +51 -4
  90. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  91. package/dist/server/lib/executionEnvironmentValidation.js +55 -3
  92. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  93. package/dist/server/lib/executionEnvironmentValidationOperations.js +155 -11
  94. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  95. package/dist/server/lib/executionEnvironments.js +11 -5
  96. package/dist/server/lib/executionEnvironments.js.map +1 -1
  97. package/dist/server/lib/executionProfileStore.js +12 -3
  98. package/dist/server/lib/executionProfileStore.js.map +1 -1
  99. package/dist/server/lib/executionProfiles.js +131 -8
  100. package/dist/server/lib/executionProfiles.js.map +1 -1
  101. package/dist/server/lib/executionReconciliation.js +206 -143
  102. package/dist/server/lib/executionReconciliation.js.map +1 -1
  103. package/dist/server/lib/executionRunAuthority.js +175 -7
  104. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  105. package/dist/server/lib/executionRuns.js +26 -4
  106. package/dist/server/lib/executionRuns.js.map +1 -1
  107. package/dist/server/lib/executionTimeline.js +2 -1
  108. package/dist/server/lib/executionTimeline.js.map +1 -1
  109. package/dist/server/lib/git.js +52 -10
  110. package/dist/server/lib/git.js.map +1 -1
  111. package/dist/server/lib/gitPushApprovals.js +367 -0
  112. package/dist/server/lib/gitPushApprovals.js.map +1 -0
  113. package/dist/server/lib/mobileAccess.js +115 -1
  114. package/dist/server/lib/mobileAccess.js.map +1 -1
  115. package/dist/server/lib/productionCheckApprovals.js +106 -0
  116. package/dist/server/lib/productionCheckApprovals.js.map +1 -0
  117. package/dist/server/lib/types.js +40 -0
  118. package/dist/server/lib/types.js.map +1 -1
  119. package/dist/server/lib/workspaceProcessEnvironment.js +2 -2
  120. package/dist/server/lib/workspaceProcessEnvironment.js.map +1 -1
  121. package/dist/server/workers/review.js +15 -9
  122. package/dist/server/workers/review.js.map +1 -1
  123. package/package.json +1 -1
  124. package/dist/client/assets/GitHubActionsDialog-BGecTzoq.js +0 -1
  125. package/dist/client/assets/VerificationDialog-D4QkMLZr.js +0 -4
  126. package/dist/client/assets/channel-BwRO4uiw.js +0 -1
  127. package/dist/client/assets/classDiagram-4FO5ZUOK-BIszcze-.js +0 -1
  128. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-D5dvxldS.js +0 -1
  129. package/dist/client/assets/index-CYPgEn1h.css +0 -2
  130. package/dist/client/assets/index-Ck2OKMsl.js +0 -276
  131. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DmICWsmn.js +0 -1
package/dist/server/db.js CHANGED
@@ -3,7 +3,7 @@ import { createHash } from 'crypto';
3
3
  import { parseCheckDefinitionsJsonStrict, parseLegacyTestCommandsJsonStrict, projectCheckDefinitionsToTestCommands, } from './lib/checkDefinitions.js';
4
4
  import { ensureParentDirectory, resolveArchonDbPath } from './lib/runtimePaths.js';
5
5
  import { TRANSCRIPT_RUN_TEXT_LIMIT_BYTES, truncateUtf8 } from './lib/transcriptRuns.js';
6
- import { createExecutionProfile, fingerprintExecutionProfile, serializeExecutionProfile, } from './lib/executionProfiles.js';
6
+ import { createExecutionProfile, EXECUTION_PROFILE_SCHEMA_VERSION, fingerprintExecutionProfile, normalizeExecutionProfile, serializeExecutionProfile, } from './lib/executionProfiles.js';
7
7
  const dbPath = ensureParentDirectory(resolveArchonDbPath());
8
8
  const db = new Database(dbPath);
9
9
  db.pragma('foreign_keys = ON');
@@ -181,7 +181,7 @@ db.exec(`
181
181
  id TEXT PRIMARY KEY,
182
182
  project_id TEXT NOT NULL,
183
183
  revision INTEGER NOT NULL CHECK (revision > 0),
184
- schema_version INTEGER NOT NULL CHECK (schema_version = 1),
184
+ schema_version INTEGER NOT NULL CHECK (schema_version IN (1, 2)),
185
185
  fingerprint TEXT NOT NULL CHECK (length(fingerprint) = 64),
186
186
  profile_json TEXT NOT NULL CHECK (
187
187
  json_valid(profile_json)
@@ -220,6 +220,46 @@ db.exec(`
220
220
  CREATE INDEX IF NOT EXISTS idx_execution_profile_audit_project
221
221
  ON execution_profile_audit_events(project_id, created_at DESC, id DESC);
222
222
  `);
223
+ const executionProfileRevisionsTableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'execution_profile_revisions'").get()?.sql ?? '';
224
+ if (!executionProfileRevisionsTableSql.includes('schema_version IN (1, 2)')) {
225
+ db.pragma('foreign_keys = OFF');
226
+ db.transaction(() => {
227
+ const referencingTriggers = db.prepare(`
228
+ SELECT name FROM sqlite_master
229
+ WHERE type = 'trigger' AND instr(sql, 'execution_profile_revisions') > 0
230
+ `).all();
231
+ for (const trigger of referencingTriggers) {
232
+ db.exec(`DROP TRIGGER IF EXISTS "${trigger.name.replaceAll('"', '""')}"`);
233
+ }
234
+ db.exec(`
235
+ CREATE TABLE execution_profile_revisions_v2 (
236
+ id TEXT PRIMARY KEY,
237
+ project_id TEXT NOT NULL,
238
+ revision INTEGER NOT NULL CHECK (revision > 0),
239
+ schema_version INTEGER NOT NULL CHECK (schema_version IN (1, 2)),
240
+ fingerprint TEXT NOT NULL CHECK (length(fingerprint) = 64),
241
+ profile_json TEXT NOT NULL CHECK (
242
+ json_valid(profile_json)
243
+ AND json_type(profile_json) = 'object'
244
+ AND length(CAST(profile_json AS BLOB)) BETWEEN 2 AND 16384
245
+ ),
246
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
247
+ UNIQUE (project_id, revision),
248
+ UNIQUE (project_id, fingerprint),
249
+ UNIQUE (project_id, id),
250
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
251
+ );
252
+ INSERT INTO execution_profile_revisions_v2 (
253
+ id, project_id, revision, schema_version, fingerprint, profile_json, created_at
254
+ )
255
+ SELECT id, project_id, revision, schema_version, fingerprint, profile_json, created_at
256
+ FROM execution_profile_revisions;
257
+ DROP TABLE execution_profile_revisions;
258
+ ALTER TABLE execution_profile_revisions_v2 RENAME TO execution_profile_revisions;
259
+ `);
260
+ })();
261
+ db.pragma('foreign_keys = ON');
262
+ }
223
263
  const backfillExecutionProfiles = () => {
224
264
  const projects = db.prepare(`
225
265
  SELECT id, build_execution_provider, active_execution_profile_revision_id
@@ -231,7 +271,7 @@ const backfillExecutionProfiles = () => {
231
271
  const insertRevision = db.prepare(`
232
272
  INSERT OR IGNORE INTO execution_profile_revisions (
233
273
  id, project_id, revision, schema_version, fingerprint, profile_json
234
- ) VALUES (?, ?, 1, 1, ?, ?)
274
+ ) VALUES (?, ?, 1, ${EXECUTION_PROFILE_SCHEMA_VERSION}, ?, ?)
235
275
  `);
236
276
  const activateRevision = db.prepare(`
237
277
  UPDATE projects SET active_execution_profile_revision_id = ? WHERE id = ?
@@ -257,7 +297,70 @@ const backfillExecutionProfiles = () => {
257
297
  }
258
298
  })();
259
299
  };
300
+ const backfillExecutionProfilesToSchemaV2 = () => {
301
+ const legacyRows = db.prepare(`
302
+ SELECT projects.id AS project_id,
303
+ revisions.id AS revision_id,
304
+ revisions.profile_json
305
+ FROM projects
306
+ INNER JOIN execution_profile_revisions revisions
307
+ ON revisions.id = projects.active_execution_profile_revision_id
308
+ AND revisions.project_id = projects.id
309
+ WHERE revisions.schema_version = 1 AND projects.deletion_started_at IS NULL
310
+ `).all();
311
+ const nextRevisionNumber = db.prepare(`
312
+ SELECT COALESCE(MAX(revision), 0) + 1 AS revision
313
+ FROM execution_profile_revisions WHERE project_id = ?
314
+ `);
315
+ const findRevisionByFingerprint = db.prepare(`
316
+ SELECT id FROM execution_profile_revisions WHERE project_id = ? AND fingerprint = ?
317
+ `);
318
+ const insertRevision = db.prepare(`
319
+ INSERT OR IGNORE INTO execution_profile_revisions (
320
+ id, project_id, revision, schema_version, fingerprint, profile_json
321
+ ) VALUES (?, ?, ?, ${EXECUTION_PROFILE_SCHEMA_VERSION}, ?, ?)
322
+ `);
323
+ const activateRevision = db.prepare(`
324
+ UPDATE projects SET active_execution_profile_revision_id = ?
325
+ WHERE id = ? AND active_execution_profile_revision_id = ?
326
+ `);
327
+ const insertEvent = db.prepare(`
328
+ INSERT OR IGNORE INTO execution_profile_audit_events (
329
+ id, project_id, event_type, profile_revision_id, previous_profile_revision_id
330
+ ) VALUES (?, ?, 'revision_activated', ?, ?)
331
+ `);
332
+ db.transaction(() => {
333
+ for (const row of legacyRows) {
334
+ let normalized;
335
+ try {
336
+ normalized = normalizeExecutionProfile(JSON.parse(row.profile_json));
337
+ }
338
+ catch {
339
+ normalized = null;
340
+ }
341
+ if (!normalized?.ok) {
342
+ throw new Error(`Stored execution profile revision ${row.revision_id} is invalid`);
343
+ }
344
+ const fingerprint = fingerprintExecutionProfile(normalized.profile);
345
+ const existing = findRevisionByFingerprint.get(row.project_id, fingerprint);
346
+ let revisionId;
347
+ if (existing) {
348
+ revisionId = existing.id;
349
+ }
350
+ else {
351
+ revisionId = `${row.project_id}:execution-profile:v2`;
352
+ const revisionNumber = nextRevisionNumber.get(row.project_id).revision;
353
+ insertRevision.run(revisionId, row.project_id, revisionNumber, fingerprint, serializeExecutionProfile(normalized.profile));
354
+ }
355
+ const activated = activateRevision.run(revisionId, row.project_id, row.revision_id);
356
+ if (activated.changes > 0) {
357
+ insertEvent.run(`${revisionId}:v2-backfill`, row.project_id, revisionId, row.revision_id);
358
+ }
359
+ }
360
+ })();
361
+ };
260
362
  backfillExecutionProfiles();
363
+ backfillExecutionProfilesToSchemaV2();
261
364
  const hostCompatibilityProfile = createExecutionProfile('host');
262
365
  const hostCompatibilityProfileJson = serializeExecutionProfile(hostCompatibilityProfile).replaceAll("'", "''");
263
366
  const hostCompatibilityFingerprint = fingerprintExecutionProfile(hostCompatibilityProfile);
@@ -272,7 +375,7 @@ db.exec(`
272
375
  INSERT INTO execution_profile_revisions (
273
376
  id, project_id, revision, schema_version, fingerprint, profile_json
274
377
  ) VALUES (
275
- NEW.id || ':execution-profile:1', NEW.id, 1, 1,
378
+ NEW.id || ':execution-profile:1', NEW.id, 1, ${EXECUTION_PROFILE_SCHEMA_VERSION},
276
379
  CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
277
380
  THEN '${dockerProfileFingerprint}' ELSE '${hostCompatibilityFingerprint}' END,
278
381
  CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
@@ -2614,6 +2717,217 @@ db.exec(`
2614
2717
  END;
2615
2718
 
2616
2719
  `);
2720
+ db.exec(`
2721
+ CREATE TABLE IF NOT EXISTS approval_requests (
2722
+ id TEXT PRIMARY KEY,
2723
+ project_id TEXT NOT NULL,
2724
+ execution_run_id TEXT NOT NULL,
2725
+ action_kind TEXT NOT NULL,
2726
+ action_schema_version INTEGER NOT NULL,
2727
+ action_json TEXT NOT NULL,
2728
+ action_fingerprint TEXT NOT NULL,
2729
+ authority_schema_version INTEGER NOT NULL,
2730
+ authority_json TEXT NOT NULL,
2731
+ status TEXT NOT NULL DEFAULT 'pending',
2732
+ expires_at DATETIME NOT NULL,
2733
+ decision_at DATETIME,
2734
+ consumed_at DATETIME,
2735
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2736
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2737
+ UNIQUE (id, project_id),
2738
+ UNIQUE (execution_run_id),
2739
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2740
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2741
+ CHECK (length(id) BETWEEN 1 AND 128),
2742
+ CHECK (action_kind IN ('git_push', 'custom_check')),
2743
+ CHECK (action_schema_version = 1),
2744
+ CHECK (length(CAST(action_json AS BLOB)) BETWEEN 2 AND 65536),
2745
+ CHECK (length(action_fingerprint) = 64 AND action_fingerprint NOT GLOB '*[^0-9a-f]*'),
2746
+ CHECK (authority_schema_version = 1),
2747
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2748
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2749
+ CHECK (status NOT IN ('approved', 'denied', 'consumed') OR decision_at IS NOT NULL),
2750
+ CHECK (status <> 'pending' OR decision_at IS NULL),
2751
+ CHECK ((status = 'consumed') = (consumed_at IS NOT NULL))
2752
+ );
2753
+
2754
+ CREATE TABLE IF NOT EXISTS approval_request_events (
2755
+ id TEXT PRIMARY KEY,
2756
+ project_id TEXT NOT NULL,
2757
+ approval_request_id TEXT NOT NULL,
2758
+ sequence INTEGER NOT NULL,
2759
+ event_type TEXT NOT NULL,
2760
+ from_status TEXT,
2761
+ to_status TEXT NOT NULL,
2762
+ actor_classification TEXT NOT NULL,
2763
+ actor_reference TEXT,
2764
+ reason TEXT,
2765
+ metadata_schema_version INTEGER NOT NULL DEFAULT 1,
2766
+ metadata_json TEXT,
2767
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2768
+ UNIQUE (approval_request_id, sequence),
2769
+ FOREIGN KEY (approval_request_id, project_id) REFERENCES approval_requests(id, project_id) ON DELETE CASCADE,
2770
+ CHECK (sequence >= 1),
2771
+ CHECK (length(event_type) BETWEEN 1 AND 100),
2772
+ CHECK (from_status IS NULL OR from_status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2773
+ CHECK (to_status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2774
+ CHECK (actor_classification IN ('human', 'system', 'action_executor')),
2775
+ CHECK (actor_reference IS NULL OR length(CAST(actor_reference AS BLOB)) BETWEEN 1 AND 200),
2776
+ CHECK (reason IS NULL OR length(CAST(reason AS BLOB)) <= 2000),
2777
+ CHECK (metadata_schema_version = 1),
2778
+ CHECK (metadata_json IS NULL OR length(CAST(metadata_json AS BLOB)) <= 4096)
2779
+ );
2780
+
2781
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_identity
2782
+ BEFORE UPDATE OF id, project_id, execution_run_id, action_kind, action_schema_version,
2783
+ action_json, action_fingerprint, authority_schema_version, authority_json, expires_at, created_at
2784
+ ON approval_requests
2785
+ BEGIN
2786
+ SELECT RAISE(ABORT, 'Approval request identity and evidence are immutable');
2787
+ END;
2788
+
2789
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_decision_evidence
2790
+ BEFORE UPDATE OF decision_at, consumed_at ON approval_requests
2791
+ WHEN (OLD.decision_at IS NOT NULL AND NEW.decision_at IS NOT OLD.decision_at)
2792
+ OR (OLD.consumed_at IS NOT NULL AND NEW.consumed_at IS NOT OLD.consumed_at)
2793
+ BEGIN
2794
+ SELECT RAISE(ABORT, 'Approval request decision evidence is immutable');
2795
+ END;
2796
+
2797
+ CREATE TRIGGER IF NOT EXISTS validate_approval_request_status_transition
2798
+ BEFORE UPDATE OF status ON approval_requests
2799
+ WHEN NOT (
2800
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'cancelled', 'expired', 'invalidated'))
2801
+ OR (OLD.status = 'approved' AND NEW.status IN ('consumed', 'expired', 'invalidated'))
2802
+ )
2803
+ BEGIN
2804
+ SELECT RAISE(ABORT, 'Invalid approval request status transition');
2805
+ END;
2806
+
2807
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_events
2808
+ BEFORE UPDATE ON approval_request_events
2809
+ BEGIN
2810
+ SELECT RAISE(ABORT, 'Approval request events are append-only');
2811
+ END;
2812
+
2813
+ CREATE TRIGGER IF NOT EXISTS prevent_approval_request_event_deletion
2814
+ BEFORE DELETE ON approval_request_events
2815
+ WHEN EXISTS (
2816
+ SELECT 1 FROM projects
2817
+ WHERE id = OLD.project_id AND deletion_started_at IS NULL
2818
+ )
2819
+ BEGIN
2820
+ SELECT RAISE(ABORT, 'Approval request events are append-only');
2821
+ END;
2822
+
2823
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_project_created
2824
+ ON approval_requests(project_id, created_at DESC, id DESC);
2825
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_project_status_expiry
2826
+ ON approval_requests(project_id, status, expires_at, id);
2827
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_action_kind
2828
+ ON approval_requests(project_id, action_kind, created_at DESC, id DESC);
2829
+ CREATE INDEX IF NOT EXISTS idx_approval_request_events_sequence
2830
+ ON approval_request_events(project_id, approval_request_id, sequence ASC);
2831
+ `);
2832
+ const migrateApprovalRequestActionLimit = () => {
2833
+ const tableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'approval_requests'").get()?.sql ?? '';
2834
+ if (tableSql.includes('BETWEEN 2 AND 65536')) {
2835
+ return;
2836
+ }
2837
+ db.exec('PRAGMA foreign_keys = OFF');
2838
+ try {
2839
+ db.exec('BEGIN');
2840
+ db.exec(`
2841
+ CREATE TABLE approval_requests_v2 (
2842
+ id TEXT PRIMARY KEY,
2843
+ project_id TEXT NOT NULL,
2844
+ execution_run_id TEXT NOT NULL,
2845
+ action_kind TEXT NOT NULL,
2846
+ action_schema_version INTEGER NOT NULL,
2847
+ action_json TEXT NOT NULL,
2848
+ action_fingerprint TEXT NOT NULL,
2849
+ authority_schema_version INTEGER NOT NULL,
2850
+ authority_json TEXT NOT NULL,
2851
+ status TEXT NOT NULL DEFAULT 'pending',
2852
+ expires_at DATETIME NOT NULL,
2853
+ decision_at DATETIME,
2854
+ consumed_at DATETIME,
2855
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2856
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2857
+ UNIQUE (id, project_id),
2858
+ UNIQUE (execution_run_id),
2859
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2860
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2861
+ CHECK (length(id) BETWEEN 1 AND 128),
2862
+ CHECK (action_kind IN ('git_push', 'custom_check')),
2863
+ CHECK (action_schema_version = 1),
2864
+ CHECK (length(CAST(action_json AS BLOB)) BETWEEN 2 AND 65536),
2865
+ CHECK (length(action_fingerprint) = 64 AND action_fingerprint NOT GLOB '*[^0-9a-f]*'),
2866
+ CHECK (authority_schema_version = 1),
2867
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2868
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2869
+ CHECK (status NOT IN ('approved', 'denied', 'consumed') OR decision_at IS NOT NULL),
2870
+ CHECK (status <> 'pending' OR decision_at IS NULL),
2871
+ CHECK ((status = 'consumed') = (consumed_at IS NOT NULL))
2872
+ );
2873
+ INSERT INTO approval_requests_v2 (
2874
+ id, project_id, execution_run_id, action_kind, action_schema_version, action_json,
2875
+ action_fingerprint, authority_schema_version, authority_json, status, expires_at,
2876
+ decision_at, consumed_at, created_at, updated_at
2877
+ )
2878
+ SELECT
2879
+ id, project_id, execution_run_id, action_kind, action_schema_version, action_json,
2880
+ action_fingerprint, authority_schema_version, authority_json, status, expires_at,
2881
+ decision_at, consumed_at, created_at, updated_at
2882
+ FROM approval_requests;
2883
+ DROP TABLE approval_requests;
2884
+ ALTER TABLE approval_requests_v2 RENAME TO approval_requests;
2885
+ `);
2886
+ db.exec('COMMIT');
2887
+ }
2888
+ catch (error) {
2889
+ db.exec('ROLLBACK');
2890
+ throw error;
2891
+ }
2892
+ finally {
2893
+ db.exec('PRAGMA foreign_keys = ON');
2894
+ }
2895
+ db.exec(`
2896
+ CREATE TRIGGER preserve_approval_request_identity
2897
+ BEFORE UPDATE OF id, project_id, execution_run_id, action_kind, action_schema_version,
2898
+ action_json, action_fingerprint, authority_schema_version, authority_json, expires_at, created_at
2899
+ ON approval_requests
2900
+ BEGIN
2901
+ SELECT RAISE(ABORT, 'Approval request identity and evidence are immutable');
2902
+ END;
2903
+
2904
+ CREATE TRIGGER preserve_approval_request_decision_evidence
2905
+ BEFORE UPDATE OF decision_at, consumed_at ON approval_requests
2906
+ WHEN (OLD.decision_at IS NOT NULL AND NEW.decision_at IS NOT OLD.decision_at)
2907
+ OR (OLD.consumed_at IS NOT NULL AND NEW.consumed_at IS NOT OLD.consumed_at)
2908
+ BEGIN
2909
+ SELECT RAISE(ABORT, 'Approval request decision evidence is immutable');
2910
+ END;
2911
+
2912
+ CREATE TRIGGER validate_approval_request_status_transition
2913
+ BEFORE UPDATE OF status ON approval_requests
2914
+ WHEN NOT (
2915
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'cancelled', 'expired', 'invalidated'))
2916
+ OR (OLD.status = 'approved' AND NEW.status IN ('consumed', 'expired', 'invalidated'))
2917
+ )
2918
+ BEGIN
2919
+ SELECT RAISE(ABORT, 'Invalid approval request status transition');
2920
+ END;
2921
+
2922
+ CREATE INDEX idx_approval_requests_project_created
2923
+ ON approval_requests(project_id, created_at DESC, id DESC);
2924
+ CREATE INDEX idx_approval_requests_project_status_expiry
2925
+ ON approval_requests(project_id, status, expires_at, id);
2926
+ CREATE INDEX idx_approval_requests_action_kind
2927
+ ON approval_requests(project_id, action_kind, created_at DESC, id DESC);
2928
+ `);
2929
+ };
2930
+ migrateApprovalRequestActionLimit();
2617
2931
  db.exec(`
2618
2932
  CREATE TABLE IF NOT EXISTS build_setup_evidence (
2619
2933
  id TEXT PRIMARY KEY,
@@ -3102,7 +3416,7 @@ db.exec(`
3102
3416
  CHECK (length(definition_fingerprint) = 64 AND definition_fingerprint NOT GLOB '*[^0-9a-f]*'),
3103
3417
  CHECK (outcome IS NULL OR outcome IN ('passed', 'command_failure', 'spawn_failure',
3104
3418
  'infrastructure_failure', 'evidence_persistence_failure', 'timeout',
3105
- 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss')),
3419
+ 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss', 'ambiguous_process_loss')),
3106
3420
  CHECK (output_revision >= 0 AND retained_start_offset >= 0 AND retained_end_offset >= retained_start_offset
3107
3421
  AND total_persisted_bytes >= retained_end_offset),
3108
3422
  CHECK (output_retention_state IN ('retained', 'expired'))
@@ -3113,14 +3427,23 @@ ensureColumn('check_attempt_details', 'retained_output', "TEXT NOT NULL DEFAULT
3113
3427
  const checkAttemptSchema = db.prepare(`
3114
3428
  SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'check_attempt_details'
3115
3429
  `).get();
3116
- if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence_failure')) {
3117
- db.transaction(() => {
3118
- db.exec(`
3119
- DROP TRIGGER IF EXISTS preserve_check_attempt_snapshots;
3120
- DROP TRIGGER IF EXISTS preserve_completed_check_attempt_details;
3121
- ALTER TABLE check_attempt_details RENAME TO check_attempt_details_legacy;
3122
-
3123
- CREATE TABLE check_attempt_details (
3430
+ if (checkAttemptSchema && (!checkAttemptSchema.sql.includes('evidence_persistence_failure')
3431
+ || !checkAttemptSchema.sql.includes('ambiguous_process_loss'))) {
3432
+ const foreignKeysEnabled = db.pragma('foreign_keys', { simple: true }) === 1;
3433
+ if (foreignKeysEnabled) {
3434
+ db.pragma('foreign_keys = OFF');
3435
+ }
3436
+ try {
3437
+ const referencingTriggers = db.prepare(`
3438
+ SELECT name FROM sqlite_master
3439
+ WHERE type = 'trigger' AND sql LIKE '%check_attempt_details%'
3440
+ `).all();
3441
+ for (const trigger of referencingTriggers) {
3442
+ db.exec(`DROP TRIGGER IF EXISTS "${trigger.name.replaceAll('"', '""')}"`);
3443
+ }
3444
+ db.transaction(() => {
3445
+ db.exec(`
3446
+ CREATE TABLE check_attempt_details_replacement (
3124
3447
  execution_run_id TEXT PRIMARY KEY,
3125
3448
  batch_run_id TEXT NOT NULL,
3126
3449
  definition_id TEXT NOT NULL,
@@ -3146,13 +3469,13 @@ if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence
3146
3469
  CHECK (length(definition_fingerprint) = 64 AND definition_fingerprint NOT GLOB '*[^0-9a-f]*'),
3147
3470
  CHECK (outcome IS NULL OR outcome IN ('passed', 'command_failure', 'spawn_failure',
3148
3471
  'infrastructure_failure', 'evidence_persistence_failure', 'timeout',
3149
- 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss')),
3472
+ 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss', 'ambiguous_process_loss')),
3150
3473
  CHECK (output_revision >= 0 AND retained_start_offset >= 0 AND retained_end_offset >= retained_start_offset
3151
3474
  AND total_persisted_bytes >= retained_end_offset),
3152
3475
  CHECK (output_retention_state IN ('retained', 'expired'))
3153
3476
  );
3154
3477
 
3155
- INSERT INTO check_attempt_details (
3478
+ INSERT INTO check_attempt_details_replacement (
3156
3479
  execution_run_id, batch_run_id, definition_id, command_snapshot_json, definition_fingerprint,
3157
3480
  initial_workspace_identity_json, final_workspace_identity_json, outcome, rerun_of_attempt_run_id,
3158
3481
  exit_code, exit_signal, exit_error, output_revision, retained_start_offset, retained_end_offset,
@@ -3163,11 +3486,22 @@ if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence
3163
3486
  initial_workspace_identity_json, final_workspace_identity_json, outcome, rerun_of_attempt_run_id,
3164
3487
  exit_code, exit_signal, exit_error, output_revision, retained_start_offset, retained_end_offset,
3165
3488
  total_persisted_bytes, retained_output, output_retention_state
3166
- FROM check_attempt_details_legacy;
3489
+ FROM check_attempt_details;
3167
3490
 
3168
- DROP TABLE check_attempt_details_legacy;
3169
- `);
3170
- })();
3491
+ DROP TABLE check_attempt_details;
3492
+ ALTER TABLE check_attempt_details_replacement RENAME TO check_attempt_details;
3493
+ `);
3494
+ })();
3495
+ }
3496
+ finally {
3497
+ if (foreignKeysEnabled) {
3498
+ db.pragma('foreign_keys = ON');
3499
+ }
3500
+ }
3501
+ const foreignKeyViolation = db.pragma('foreign_key_check')[0];
3502
+ if (foreignKeyViolation) {
3503
+ throw new Error(`Check attempt migration left an invalid foreign key: ${JSON.stringify(foreignKeyViolation)}`);
3504
+ }
3171
3505
  }
3172
3506
  db.exec(`
3173
3507
  CREATE TABLE IF NOT EXISTS check_attempt_diagnostic_parsers (