@straiffi/archon 1.8.20 → 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 (170) hide show
  1. package/dist/client/assets/{ActivityView-DDE3U1zB.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-oPUmRpZr.js → architectureDiagram-3BPJPVTR-BrZ08mqq.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-0sq1jkkp.js → blockDiagram-GPEHLZMM-b93XwJ6Z.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-BeDxMy7J.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-D8VGsje2.js → chunk-2J33WTMH-D7Vxn_1m.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DuTUTeBe.js → chunk-3OPIFGDE-B1zo2UZq.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BzX5dPNW.js → chunk-5ZQYHXKU-BajvkDdQ.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DS9putxN.js → chunk-727SXJPM-CSH7cVpV.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CWzFZRBv.js → chunk-AQP2D5EJ-DE-wgUNP.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DXZ-ADYW.js → chunk-CSCIHK7Q-Ba7JgCEf.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-BD7mL3KK.js → chunk-KSCS5N6A-0o3YWkHM.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DmYQi9AP.js → chunk-L5ZTLDWV-DihWczP8.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-CZyFYHvS.js → chunk-LZXEDZCA-Bxnu-hme.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-BxdjvT9I.js → chunk-ND2GUHAM-CoaR5kmP.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-BwuZcZ_9.js → chunk-NZK2D7GU-By92Rd3d.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-CC-TZXr3.js → chunk-O5CBEL6O-D83Pkgfu.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CJZJkca2.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-D2L5RvPF.js → dagre-BM42HDAG-Dq1ALF7Z.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DBJzxSqZ.js → diagram-2AECGRRQ-1t1_TKtu.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-J_1M-Wou.js → diagram-5GNKFQAL-DtvYRL6f.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-CmT1Vbmc.js → diagram-KO2AKTUF-PRNrSX5F.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-DfPoJyQI.js → diagram-LMA3HP47-DaTX0DhT.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-D4uEb6vo.js → diagram-OG6HWLK6-C2p9p-92.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-l7yIFj0e.js → erDiagram-TEJ5UH35-DKzLtnp-.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-B3caTMes.js → flowDiagram-I6XJVG4X-GdL8lm6e.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-9Am36v9m.js → ganttDiagram-6RSMTGT7-Ok-4XYwT.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-B7OjYsv_.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-ZAcNejkW.js → infoDiagram-5YYISTIA-Dl4hRXis.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BaaJwuN0.js → ishikawaDiagram-YF4QCWOH-C1Dc1fTT.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-DjzTU3kM.js → journeyDiagram-JHISSGLW-CUscmJze.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-DYRchzub.js → kanban-definition-UN3LZRKU-C0nwYjQV.js} +1 -1
  39. package/dist/client/assets/{line-X0j9gj61.js → line-ByiuEIg_.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-DCKG87Vl.js → mermaid-parser.core-B-XdAyDR.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-BxI5T2gs.js → mermaid.core-D_bIfuZD.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BE7MZzQO.js → mindmap-definition-RKZ34NQL-BGNiPJDK.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-D3fTYovg.js → pieDiagram-4H26LBE5-CP6FpAoO.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-B35nXRdQ.js → quadrantDiagram-W4KKPZXB-dsAWBAQn.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CLBcHvP0.js → requirementDiagram-4Y6WPE33-DJH-zHqF.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DdBCxGHF.js → sankeyDiagram-5OEKKPKP-BJMnZrqH.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-KcLt0X1K.js → sequenceDiagram-3UESZ5HK-CzNjUSlM.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-Bq23ObLK.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-SK3oVUD9.js → timeline-definition-PNZ67QCA-BX2CI5H_.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-COVtAlEC.js → vennDiagram-CIIHVFJN-Bd1KZaXh.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BvfOOwga.js → wardleyDiagram-YWT4CUSO-D-0BNz49.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DFnAD9k4.js → xychartDiagram-2RQKCTM6-DbYa0ZWK.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +780 -16
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +198 -20
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +8 -4
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentProvider.js +8 -1
  62. package/dist/server/lib/agentProvider.js.map +1 -1
  63. package/dist/server/lib/approvalReconciliation.js +101 -0
  64. package/dist/server/lib/approvalReconciliation.js.map +1 -0
  65. package/dist/server/lib/approvalRequests.js +587 -0
  66. package/dist/server/lib/approvalRequests.js.map +1 -0
  67. package/dist/server/lib/approvalRoutes.js +143 -0
  68. package/dist/server/lib/approvalRoutes.js.map +1 -0
  69. package/dist/server/lib/automationProgression.js +44 -14
  70. package/dist/server/lib/automationProgression.js.map +1 -1
  71. package/dist/server/lib/buildCompletionContinuation.js +14 -3
  72. package/dist/server/lib/buildCompletionContinuation.js.map +1 -1
  73. package/dist/server/lib/buildExecutionEnvironment.js +350 -66
  74. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  75. package/dist/server/lib/buildOpenQuestionOperations.js +47 -3
  76. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
  77. package/dist/server/lib/buildSequences.js +267 -18
  78. package/dist/server/lib/buildSequences.js.map +1 -1
  79. package/dist/server/lib/buildSetupAuthority.js +138 -0
  80. package/dist/server/lib/buildSetupAuthority.js.map +1 -0
  81. package/dist/server/lib/bundleReviewOperations.js +15 -4
  82. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  83. package/dist/server/lib/checkDefinitions.js +29 -1
  84. package/dist/server/lib/checkDefinitions.js.map +1 -1
  85. package/dist/server/lib/checkRunner.js +306 -27
  86. package/dist/server/lib/checkRunner.js.map +1 -1
  87. package/dist/server/lib/checkRuns.js +4 -2
  88. package/dist/server/lib/checkRuns.js.map +1 -1
  89. package/dist/server/lib/consequentialActionAuthority.js +142 -0
  90. package/dist/server/lib/consequentialActionAuthority.js.map +1 -0
  91. package/dist/server/lib/consequentialActions.js +231 -0
  92. package/dist/server/lib/consequentialActions.js.map +1 -0
  93. package/dist/server/lib/dockerSandboxCredentials.js +64 -10
  94. package/dist/server/lib/dockerSandboxCredentials.js.map +1 -1
  95. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +119 -0
  96. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  97. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +174 -11
  98. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  99. package/dist/server/lib/dockerSandboxExecutionProvider.js +12 -3
  100. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  101. package/dist/server/lib/dockerSandboxNetworkPolicy.js +222 -0
  102. package/dist/server/lib/dockerSandboxNetworkPolicy.js.map +1 -0
  103. package/dist/server/lib/executionCredentialCapabilities.js +106 -0
  104. package/dist/server/lib/executionCredentialCapabilities.js.map +1 -0
  105. package/dist/server/lib/executionEnvironment.js.map +1 -1
  106. package/dist/server/lib/executionEnvironmentContexts.js +59 -3
  107. package/dist/server/lib/executionEnvironmentContexts.js.map +1 -1
  108. package/dist/server/lib/executionEnvironmentLifecycle.js +34 -0
  109. package/dist/server/lib/executionEnvironmentLifecycle.js.map +1 -1
  110. package/dist/server/lib/executionEnvironmentRoutes.js +127 -9
  111. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  112. package/dist/server/lib/executionEnvironmentValidation.js +169 -45
  113. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  114. package/dist/server/lib/executionEnvironmentValidationOperations.js +203 -19
  115. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  116. package/dist/server/lib/executionEnvironments.js +103 -14
  117. package/dist/server/lib/executionEnvironments.js.map +1 -1
  118. package/dist/server/lib/executionProfileRoutes.js +67 -0
  119. package/dist/server/lib/executionProfileRoutes.js.map +1 -0
  120. package/dist/server/lib/executionProfileStore.js +117 -0
  121. package/dist/server/lib/executionProfileStore.js.map +1 -0
  122. package/dist/server/lib/executionProfiles.js +324 -0
  123. package/dist/server/lib/executionProfiles.js.map +1 -0
  124. package/dist/server/lib/executionReconciliation.js +345 -197
  125. package/dist/server/lib/executionReconciliation.js.map +1 -1
  126. package/dist/server/lib/executionRetry.js +24 -5
  127. package/dist/server/lib/executionRetry.js.map +1 -1
  128. package/dist/server/lib/executionRunAuthority.js +467 -0
  129. package/dist/server/lib/executionRunAuthority.js.map +1 -0
  130. package/dist/server/lib/executionRuns.js +30 -4
  131. package/dist/server/lib/executionRuns.js.map +1 -1
  132. package/dist/server/lib/executionTimeline.js +2 -1
  133. package/dist/server/lib/executionTimeline.js.map +1 -1
  134. package/dist/server/lib/git.js +77 -10
  135. package/dist/server/lib/git.js.map +1 -1
  136. package/dist/server/lib/gitPushApprovals.js +367 -0
  137. package/dist/server/lib/gitPushApprovals.js.map +1 -0
  138. package/dist/server/lib/hostExecutionEnvironment.js +7 -1
  139. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -1
  140. package/dist/server/lib/mobileAccess.js +115 -1
  141. package/dist/server/lib/mobileAccess.js.map +1 -1
  142. package/dist/server/lib/productionCheckApprovals.js +106 -0
  143. package/dist/server/lib/productionCheckApprovals.js.map +1 -0
  144. package/dist/server/lib/projects.js +1 -0
  145. package/dist/server/lib/projects.js.map +1 -1
  146. package/dist/server/lib/run.js +58 -2
  147. package/dist/server/lib/run.js.map +1 -1
  148. package/dist/server/lib/ticketFollowUpOperations.js +32 -6
  149. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  150. package/dist/server/lib/transcriptRuns.js +3 -0
  151. package/dist/server/lib/transcriptRuns.js.map +1 -1
  152. package/dist/server/lib/types.js +40 -0
  153. package/dist/server/lib/types.js.map +1 -1
  154. package/dist/server/lib/workspaceProcessEnvironment.js +72 -1
  155. package/dist/server/lib/workspaceProcessEnvironment.js.map +1 -1
  156. package/dist/server/workers/build.js +214 -46
  157. package/dist/server/workers/build.js.map +1 -1
  158. package/dist/server/workers/followUp.js +74 -1
  159. package/dist/server/workers/followUp.js.map +1 -1
  160. package/dist/server/workers/review.js +30 -13
  161. package/dist/server/workers/review.js.map +1 -1
  162. package/package.json +1 -1
  163. package/dist/client/assets/GitHubActionsDialog-CM1z-amZ.js +0 -1
  164. package/dist/client/assets/VerificationDialog-oHjNllh6.js +0 -4
  165. package/dist/client/assets/channel-CF8hkOdu.js +0 -1
  166. package/dist/client/assets/classDiagram-4FO5ZUOK-D6FiG9_B.js +0 -1
  167. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bim05ijr.js +0 -1
  168. package/dist/client/assets/index-CsZfTdMO.css +0 -2
  169. package/dist/client/assets/index-DDefs3iM.js +0 -275
  170. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BLzZdVo0.js +0 -1
@@ -1,15 +1,20 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { spawn } from 'child_process';
3
+ import db from '../db.js';
3
4
  import { existsSync, lstatSync, realpathSync } from 'fs';
4
- import { relative, resolve } from 'path';
5
+ import { basename, relative, resolve } from 'path';
5
6
  import { StringDecoder } from 'string_decoder';
6
- import { findActiveCheckBatchForTarget, findActiveCheckAutoFixForTarget, canAutoFixStartCheckBatch, getCheckBatchDetails, inspectCheckBatchAggregation, listCheckAttemptDetails, rerunCheckBatch, setCheckBatchStopRequested, } from './checkRuns.js';
7
+ import { findActiveCheckBatchForTarget, findActiveCheckAutoFixForTarget, canAutoFixStartCheckBatch, getCheckBatchDetails, getCheckAttemptDetails, inspectCheckBatchAggregation, listCheckAttemptDetails, rerunCheckBatch, setCheckBatchStopRequested, } from './checkRuns.js';
7
8
  import { getCheckDefinitionFingerprint } from './checkDefinitions.js';
8
9
  import { captureCheckWorkspaceIdentity } from './checkWorkspaceIdentity.js';
9
10
  import { getProjectById, getProjectCheckDefinitions } from './projects.js';
10
11
  import { aggregateCheckBatch, completeCheckAttempt, startCheckAttempt, withExecutionRunTransaction, } from './executionRuns.js';
11
12
  import { resolveCommandWorkspacePath } from './testCommandRunner.js';
12
13
  import { createWorkspaceProcessEnvironment } from './workspaceProcessEnvironment.js';
14
+ import { createGovernedHostRuntimeEnvironment } from './workspaceProcessEnvironment.js';
15
+ import { cancelApprovalRequest, consumeApprovalRequest, createApprovalRequestInTransaction, emitApprovalRequestCreated, getApprovalRequest, invalidateApprovalRequest, registerApprovalRequestTerminalHandler, } from './approvalRequests.js';
16
+ import { fingerprintConsequentialAction } from './consequentialActions.js';
17
+ import { buildProductionCheckAction, PRODUCTION_CHECK_APPROVAL_TTL_MS } from './productionCheckApprovals.js';
13
18
  import { appendCheckAttemptEvidence, appendCheckDiagnostics, completeCheckDiagnosticParser, initializeCheckDiagnosticParser, } from './checkDiagnostics.js';
14
19
  import { createCheckDiagnosticParserSession } from './checkDiagnosticParsers.js';
15
20
  const MAX_CONCURRENT_CHECK_ATTEMPTS = 2;
@@ -35,7 +40,7 @@ const processes = new Map();
35
40
  const admittedEntries = new Map();
36
41
  const invalidatedBatches = new Set();
37
42
  const stoppedBatches = new Set();
38
- const forcedBatchOutcomes = new Map();
43
+ const forcedAttemptOutcomes = new Map();
39
44
  const batchWaiters = new Map();
40
45
  let admittedAttempts = 0;
41
46
  let acceptingAdmissions = true;
@@ -72,8 +77,26 @@ export const openCheckRunnerAdmissions = () => {
72
77
  acceptingAdmissions = true;
73
78
  };
74
79
  const resolveShell = () => process.env.SHELL?.trim() || '/bin/zsh';
75
- const createShellEnvironment = (cwd) => {
76
- const env = createWorkspaceProcessEnvironment(cwd);
80
+ export const getCheckShellArguments = (shell, command, governedProductionEnvironment = false) => {
81
+ if (!governedProductionEnvironment) {
82
+ return ['-i', '-c', command];
83
+ }
84
+ const shellName = basename(shell);
85
+ if (shellName === 'zsh') {
86
+ return ['-f', '-c', command];
87
+ }
88
+ if (shellName === 'bash') {
89
+ return ['--noprofile', '--norc', '-c', command];
90
+ }
91
+ if (shellName === 'fish') {
92
+ return ['--no-config', '-c', command];
93
+ }
94
+ return ['-c', command];
95
+ };
96
+ const createShellEnvironment = (cwd, definition, governedProductionEnvironment = false) => {
97
+ const env = definition.consequence === 'production' && governedProductionEnvironment
98
+ ? createGovernedHostRuntimeEnvironment(cwd).variables
99
+ : createWorkspaceProcessEnvironment(cwd);
77
100
  delete env.PORT;
78
101
  return env;
79
102
  };
@@ -84,7 +107,7 @@ const isPathInside = (root, candidate) => {
84
107
  const pathFromRoot = relative(root, candidate).replaceAll('\\', '/');
85
108
  return pathFromRoot === '' || (!pathFromRoot.startsWith('..') && !pathFromRoot.includes('/../'));
86
109
  };
87
- const resolveDefinitionCwd = (workspaceCwd, definition) => {
110
+ export const resolveDefinitionCwd = (workspaceCwd, definition) => {
88
111
  const workspacePath = resolveCommandWorkspacePath(definition);
89
112
  if (!workspacePath) {
90
113
  return workspaceCwd;
@@ -300,6 +323,26 @@ const terminateProcess = (record) => {
300
323
  record.killTimer = setTimeout(() => signalProcessGroup(record.child, 'SIGKILL'), STOP_GRACE_MS);
301
324
  }
302
325
  };
326
+ const waitForProcessFinalization = (record) => {
327
+ if (record.finalizationPromise) {
328
+ return record.finalizationPromise;
329
+ }
330
+ return new Promise((resolve, reject) => {
331
+ const waitForFinalization = () => {
332
+ const finalizationPromise = record.finalizationPromise;
333
+ if (!finalizationPromise) {
334
+ reject(new Error(`Check attempt ${record.attemptRunId} closed without starting finalization`));
335
+ return;
336
+ }
337
+ void finalizationPromise.then(resolve, reject);
338
+ };
339
+ record.child.once('close', waitForFinalization);
340
+ if (record.finalizationPromise) {
341
+ record.child.off('close', waitForFinalization);
342
+ void record.finalizationPromise.then(resolve, reject);
343
+ }
344
+ });
345
+ };
303
346
  const notifyBatchWaiters = (projectId, batchRunId) => {
304
347
  const details = getCheckBatchDetails(projectId, batchRunId);
305
348
  if (!details || !['completed', 'failed', 'cancelled', 'interrupted'].includes(details.execution_status)) {
@@ -418,7 +461,7 @@ const failQueuedBatch = async (batchRunId, outcome, identity) => {
418
461
  };
419
462
  const runQueuedAttempt = async (entry) => {
420
463
  const identity = await captureCheckWorkspaceIdentity(entry.cwd);
421
- const forcedOutcome = forcedBatchOutcomes.get(entry.batchRunId);
464
+ const forcedOutcome = forcedAttemptOutcomes.get(entry.attemptRunId);
422
465
  if (forcedOutcome) {
423
466
  await completeAttempt(entry, forcedOutcome, identity, {
424
467
  exitError: forcedOutcome === 'shutdown_loss'
@@ -440,6 +483,30 @@ const runQueuedAttempt = async (entry) => {
440
483
  return;
441
484
  }
442
485
  const spawnCwd = resolveDefinitionCwd(entry.cwd, entry.definition);
486
+ if (entry.approvalRequestId) {
487
+ const request = getApprovalRequest(entry.projectId, entry.approvalRequestId, { emitter: entry.emitter });
488
+ const batch = getCheckBatchDetails(entry.projectId, entry.batchRunId);
489
+ if (!request || request.status !== 'consumed' || !batch) {
490
+ throw new Error('Production check approval is no longer consumable');
491
+ }
492
+ const currentDefinitions = getProjectCheckDefinitions(getProjectById(entry.projectId));
493
+ const currentDefinition = currentDefinitions.find(definition => definition.id === entry.definition.id);
494
+ if (!currentDefinition || getCheckDefinitionFingerprint(currentDefinition) !== getCheckDefinitionFingerprint(entry.definition)) {
495
+ throw new Error('Production check definition changed before spawn');
496
+ }
497
+ const current = buildProductionCheckAction({
498
+ projectId: entry.projectId,
499
+ workspaceCwd: entry.cwd,
500
+ effectiveCwd: spawnCwd,
501
+ definition: currentDefinition,
502
+ batch,
503
+ workspaceIdentity: identity,
504
+ expiresAt: request.expires_at,
505
+ });
506
+ if (fingerprintConsequentialAction(current.action) !== request.action_fingerprint) {
507
+ throw new Error('Production check action changed before spawn');
508
+ }
509
+ }
443
510
  initializeCheckDiagnosticParser(entry.projectId, entry.attemptRunId);
444
511
  startCheckAttempt({
445
512
  projectId: entry.projectId,
@@ -448,10 +515,11 @@ const runQueuedAttempt = async (entry) => {
448
515
  await new Promise((resolveAttempt, rejectAttempt) => {
449
516
  let child;
450
517
  try {
451
- child = spawn(resolveShell(), ['-i', '-c', entry.definition.command], {
518
+ const shell = resolveShell();
519
+ child = spawn(shell, getCheckShellArguments(shell, entry.definition.command, entry.governedProductionEnvironment), {
452
520
  cwd: spawnCwd,
453
521
  detached: process.platform !== 'win32',
454
- env: createShellEnvironment(entry.cwd),
522
+ env: createShellEnvironment(entry.cwd, entry.definition, entry.governedProductionEnvironment),
455
523
  stdio: ['ignore', 'pipe', 'pipe'],
456
524
  });
457
525
  }
@@ -559,6 +627,8 @@ const enqueueCheckAttempts = (input) => {
559
627
  definition: attempt.definition,
560
628
  initialIdentity: input.initialIdentity,
561
629
  emitter: input.emitter,
630
+ approvalRequestId: attempt.approvalRequestId,
631
+ governedProductionEnvironment: attempt.governedProductionEnvironment,
562
632
  })));
563
633
  pumpScheduler();
564
634
  };
@@ -608,6 +678,7 @@ export const startCheckBatch = async (input, emitter) => withCheckAdmission(asyn
608
678
  assertAcceptingAdmissions();
609
679
  const batchRunId = randomUUID();
610
680
  const attempts = input.selectedDefinitions.map(definition => ({ runId: randomUUID(), definition }));
681
+ const createdApprovals = [];
611
682
  const checkTarget = input.targetKind === 'bundle'
612
683
  ? { targetKind: 'bundle', targetBundleId: targetBundleId }
613
684
  : { targetKind: 'repo_root', targetBundleId: null };
@@ -665,6 +736,26 @@ export const startCheckBatch = async (input, emitter) => withCheckAdmission(asyn
665
736
  },
666
737
  });
667
738
  }
739
+ const batch = getCheckBatchDetails(input.projectId, batchRunId);
740
+ for (const attempt of attempts.filter(candidate => candidate.definition.consequence === 'production')) {
741
+ const expiresAt = new Date(Date.now() + PRODUCTION_CHECK_APPROVAL_TTL_MS).toISOString();
742
+ const approval = buildProductionCheckAction({
743
+ projectId: input.projectId,
744
+ workspaceCwd: input.cwd,
745
+ effectiveCwd: resolveDefinitionCwd(input.cwd, attempt.definition),
746
+ definition: attempt.definition,
747
+ batch,
748
+ workspaceIdentity: initialIdentity,
749
+ expiresAt,
750
+ });
751
+ createdApprovals.push(createApprovalRequestInTransaction({
752
+ projectId: input.projectId,
753
+ executionRunId: attempt.runId,
754
+ action: approval.action,
755
+ authority: approval.authority,
756
+ expiresAt,
757
+ }, transaction));
758
+ }
668
759
  input.onBatchCreated?.(batchRunId);
669
760
  transaction.transitionExecutionRun({
670
761
  projectId: input.projectId,
@@ -674,11 +765,15 @@ export const startCheckBatch = async (input, emitter) => withCheckAdmission(asyn
674
765
  eventType: 'check_batch_started',
675
766
  });
676
767
  }, emitter);
768
+ for (const approval of createdApprovals) {
769
+ emitApprovalRequestCreated(emitter, approval);
770
+ }
771
+ const ordinaryAttempts = attempts.filter(attempt => attempt.definition.consequence !== 'production');
677
772
  enqueueCheckAttempts({
678
773
  projectId: input.projectId,
679
774
  batchRunId,
680
775
  cwd: input.cwd,
681
- attempts,
776
+ attempts: ordinaryAttempts,
682
777
  initialIdentity,
683
778
  emitter,
684
779
  });
@@ -711,19 +806,47 @@ export const rerunCheckBatchAndSchedule = async (input, emitter) => withCheckAdm
711
806
  }
712
807
  }
713
808
  assertAcceptingAdmissions();
714
- const created = withExecutionRunTransaction(transaction => rerunCheckBatch({
715
- transaction,
716
- projectId: input.projectId,
717
- sourceBatchRunId: input.sourceBatchRunId,
718
- selection: input.selection,
719
- availableDefinitions: currentDefinitions,
720
- initialWorkspaceIdentity: initialIdentity,
721
- }), emitter);
809
+ const createdApprovals = [];
810
+ const created = withExecutionRunTransaction(transaction => {
811
+ const result = rerunCheckBatch({
812
+ transaction,
813
+ projectId: input.projectId,
814
+ sourceBatchRunId: input.sourceBatchRunId,
815
+ selection: input.selection,
816
+ availableDefinitions: currentDefinitions,
817
+ initialWorkspaceIdentity: initialIdentity,
818
+ });
819
+ const batch = getCheckBatchDetails(input.projectId, result.batchRunId);
820
+ for (const attempt of result.attempts.filter(candidate => candidate.definition.consequence === 'production')) {
821
+ const expiresAt = new Date(Date.now() + PRODUCTION_CHECK_APPROVAL_TTL_MS).toISOString();
822
+ const approval = buildProductionCheckAction({
823
+ projectId: input.projectId,
824
+ workspaceCwd: input.cwd,
825
+ effectiveCwd: resolveDefinitionCwd(input.cwd, attempt.definition),
826
+ definition: attempt.definition,
827
+ batch,
828
+ workspaceIdentity: initialIdentity,
829
+ expiresAt,
830
+ });
831
+ createdApprovals.push(createApprovalRequestInTransaction({
832
+ projectId: input.projectId,
833
+ executionRunId: attempt.runId,
834
+ action: approval.action,
835
+ authority: approval.authority,
836
+ expiresAt,
837
+ }, transaction));
838
+ }
839
+ return result;
840
+ }, emitter);
841
+ for (const approval of createdApprovals) {
842
+ emitApprovalRequestCreated(emitter, approval);
843
+ }
844
+ const ordinaryAttempts = created.attempts.filter(attempt => attempt.definition.consequence !== 'production');
722
845
  enqueueCheckAttempts({
723
846
  projectId: input.projectId,
724
847
  batchRunId: created.batchRunId,
725
848
  cwd: input.cwd,
726
- attempts: created.attempts,
849
+ attempts: ordinaryAttempts,
727
850
  initialIdentity,
728
851
  emitter,
729
852
  });
@@ -742,6 +865,21 @@ export const stopCheckBatch = async (projectId, batchRunId, emitter) => {
742
865
  }
743
866
  const batch = setCheckBatchStopRequested(projectId, batchRunId);
744
867
  stoppedBatches.add(batchRunId);
868
+ const approvalRows = db.prepare(`
869
+ SELECT request.id
870
+ FROM approval_requests request
871
+ JOIN check_attempt_details attempt ON attempt.execution_run_id = request.execution_run_id
872
+ WHERE request.project_id = ? AND attempt.batch_run_id = ?
873
+ AND request.status IN ('pending', 'approved')
874
+ `).all(projectId, batchRunId);
875
+ for (const row of approvalRows) {
876
+ cancelApprovalRequest({
877
+ projectId,
878
+ requestId: row.id,
879
+ reason: 'check_batch_stopped',
880
+ emitter,
881
+ });
882
+ }
745
883
  const identity = await captureCheckWorkspaceIdentity([...processes.values()].find(record => record.batchRunId === batchRunId)?.cwd
746
884
  ?? queue.find(entry => entry.batchRunId === batchRunId)?.cwd
747
885
  ?? '');
@@ -775,30 +913,169 @@ export const getCheckRunnerState = () => ({
775
913
  queued_attempt_count: queue.length,
776
914
  process_attempt_ids: [...processes.keys()],
777
915
  });
916
+ export const resumeProductionCheckApproval = async (projectId, requestId, emitter) => withCheckAdmission(async () => {
917
+ const request = getApprovalRequest(projectId, requestId, { emitter });
918
+ if (!request || request.status !== 'approved' || request.action.kind !== 'custom_check') {
919
+ return false;
920
+ }
921
+ const attempt = getCheckAttemptDetails(projectId, request.execution_run_id);
922
+ if (!attempt || attempt.outcome) {
923
+ return false;
924
+ }
925
+ const batch = getCheckBatchDetails(projectId, attempt.batch_run_id);
926
+ if (!batch || batch.stop_requested || ['completed', 'failed', 'cancelled', 'interrupted'].includes(batch.execution_status)) {
927
+ invalidateApprovalRequest({ projectId, requestId, reason: 'check_batch_no_longer_active', emitter });
928
+ return false;
929
+ }
930
+ const workspaceCwd = request.action.workspace_git.workspace_path;
931
+ let currentIdentity;
932
+ let currentDefinition;
933
+ try {
934
+ currentIdentity = await captureCheckWorkspaceIdentity(workspaceCwd);
935
+ currentDefinition = getProjectCheckDefinitions(getProjectById(projectId))
936
+ .find(definition => definition.id === attempt.definition_id);
937
+ }
938
+ catch {
939
+ invalidateApprovalRequest({ projectId, requestId, reason: 'production_check_revalidation_failed', emitter });
940
+ return false;
941
+ }
942
+ if (!currentDefinition
943
+ || currentDefinition.category !== 'custom'
944
+ || currentDefinition.consequence !== 'production'
945
+ || getCheckDefinitionFingerprint(currentDefinition) !== attempt.definition_fingerprint) {
946
+ consumeApprovalRequest({
947
+ projectId,
948
+ requestId,
949
+ currentAction: {
950
+ ...request.action,
951
+ target: { ...request.action.target, check_definition_fingerprint: '0'.repeat(64) },
952
+ },
953
+ emitter,
954
+ });
955
+ return false;
956
+ }
957
+ let current;
958
+ try {
959
+ current = buildProductionCheckAction({
960
+ projectId,
961
+ workspaceCwd,
962
+ effectiveCwd: resolveDefinitionCwd(workspaceCwd, currentDefinition),
963
+ definition: currentDefinition,
964
+ batch,
965
+ workspaceIdentity: currentIdentity,
966
+ expiresAt: request.expires_at,
967
+ });
968
+ }
969
+ catch {
970
+ invalidateApprovalRequest({ projectId, requestId, reason: 'production_check_revalidation_failed', emitter });
971
+ return false;
972
+ }
973
+ const consumed = consumeApprovalRequest({ projectId, requestId, currentAction: current.action, emitter });
974
+ if (!consumed.ok) {
975
+ if (consumed.request && ['expired', 'invalidated'].includes(consumed.request.status)) {
976
+ await finalizeProductionCheckApproval(projectId, requestId, emitter);
977
+ }
978
+ return false;
979
+ }
980
+ enqueueCheckAttempts({
981
+ projectId,
982
+ batchRunId: batch.execution_run_id,
983
+ cwd: workspaceCwd,
984
+ attempts: [{
985
+ runId: attempt.execution_run_id,
986
+ definition: currentDefinition,
987
+ approvalRequestId: requestId,
988
+ governedProductionEnvironment: request.authority.selected_profile.compatibility === 'governed',
989
+ }],
990
+ initialIdentity: attempt.initial_workspace_identity,
991
+ emitter,
992
+ });
993
+ return true;
994
+ });
995
+ export const finalizeProductionCheckApproval = (projectId, requestId, emitter) => {
996
+ const request = getApprovalRequest(projectId, requestId, { emitter });
997
+ if (!request || request.action.kind !== 'custom_check' || !['denied', 'cancelled', 'expired', 'invalidated'].includes(request.status)) {
998
+ return;
999
+ }
1000
+ const attempt = getCheckAttemptDetails(projectId, request.execution_run_id);
1001
+ if (!attempt) {
1002
+ return;
1003
+ }
1004
+ aggregateCheckBatch(projectId, attempt.batch_run_id, emitter);
1005
+ notifyBatchWaiters(projectId, attempt.batch_run_id);
1006
+ };
1007
+ registerApprovalRequestTerminalHandler((request, emitter) => {
1008
+ if (request.action_kind === 'custom_check') {
1009
+ finalizeProductionCheckApproval(request.project_id, request.id, emitter);
1010
+ }
1011
+ });
778
1012
  export const shutdownCheckRunner = async () => {
779
1013
  closeCheckRunnerAdmissions();
780
1014
  await waitForCheckAdmissions();
1015
+ const hasApprovalRequests = Boolean(db.prepare(`
1016
+ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'approval_requests'
1017
+ `).get());
1018
+ const durableAttempts = db.prepare(`
1019
+ SELECT run.project_id, run.id AS attempt_run_id, run.parent_run_id AS batch_run_id,
1020
+ ${hasApprovalRequests ? 'approval.id' : 'NULL'} AS approval_request_id,
1021
+ ${hasApprovalRequests ? 'approval.status' : 'NULL'} AS approval_status
1022
+ FROM execution_runs run
1023
+ ${hasApprovalRequests ? `LEFT JOIN approval_requests approval
1024
+ ON approval.execution_run_id = run.id AND approval.project_id = run.project_id` : ''}
1025
+ WHERE run.kind = 'check_attempt' AND run.status IN ('queued', 'waiting', 'running')
1026
+ `).all();
781
1027
  const batchIds = new Set([
782
1028
  ...queue.map(entry => entry.batchRunId),
783
1029
  ...[...admittedEntries.values()].map(entry => entry.batchRunId),
784
1030
  ...[...processes.values()].map(record => record.batchRunId),
1031
+ ...durableAttempts.map(attempt => attempt.batch_run_id),
785
1032
  ]);
1033
+ const approvalHeldBatchIds = new Set(durableAttempts
1034
+ .filter(attempt => attempt.approval_status === 'pending' || attempt.approval_status === 'approved')
1035
+ .map(attempt => attempt.batch_run_id));
1036
+ const activeRecords = [...processes.values()];
786
1037
  try {
787
- for (const batchRunId of batchIds) {
788
- forcedBatchOutcomes.set(batchRunId, 'shutdown_loss');
1038
+ for (const entry of admittedEntries.values()) {
1039
+ forcedAttemptOutcomes.set(entry.attemptRunId, entry.approvalRequestId ? 'ambiguous_process_loss' : 'shutdown_loss');
789
1040
  }
790
1041
  const queuedEntries = queue.splice(0);
791
1042
  for (const entry of queuedEntries) {
792
1043
  const finalIdentity = await captureCheckWorkspaceIdentity(entry.cwd);
793
- await completeAttempt(entry, 'shutdown_loss', finalIdentity, {
794
- exitError: 'The server shut down before this check attempt started',
1044
+ const outcome = entry.approvalRequestId ? 'ambiguous_process_loss' : 'shutdown_loss';
1045
+ await completeAttempt(entry, outcome, finalIdentity, {
1046
+ exitError: entry.approvalRequestId
1047
+ ? 'The consumed production command process identity was lost during shutdown'
1048
+ : 'The server shut down before this check attempt started',
795
1049
  });
796
1050
  }
797
- for (const record of processes.values()) {
798
- record.terminalCause ??= 'shutdown_loss';
1051
+ for (const attempt of durableAttempts) {
1052
+ if (attempt.approval_request_id && (attempt.approval_status === 'pending' || attempt.approval_status === 'approved')) {
1053
+ continue;
1054
+ }
1055
+ if (admittedEntries.has(attempt.attempt_run_id)) {
1056
+ continue;
1057
+ }
1058
+ const details = getCheckAttemptDetails(attempt.project_id, attempt.attempt_run_id);
1059
+ if (details && !details.outcome) {
1060
+ completeCheckAttempt({
1061
+ projectId: attempt.project_id,
1062
+ runId: attempt.attempt_run_id,
1063
+ outcome: attempt.approval_status === 'consumed' ? 'ambiguous_process_loss' : 'shutdown_loss',
1064
+ finalWorkspaceIdentity: details.initial_workspace_identity,
1065
+ exitError: attempt.approval_status === 'consumed'
1066
+ ? 'The consumed production command process identity was lost during shutdown'
1067
+ : 'The server shut down before this check attempt completed',
1068
+ });
1069
+ aggregateCheckBatch(attempt.project_id, attempt.batch_run_id);
1070
+ notifyBatchWaiters(attempt.project_id, attempt.batch_run_id);
1071
+ }
1072
+ }
1073
+ for (const record of activeRecords) {
1074
+ record.terminalCause ??= record.approvalRequestId ? 'ambiguous_process_loss' : 'shutdown_loss';
799
1075
  terminateProcess(record);
800
1076
  }
801
- await Promise.all([...batchIds].map(batchRunId => {
1077
+ await Promise.all(activeRecords.map(waitForProcessFinalization));
1078
+ await Promise.all([...batchIds].filter(batchRunId => !approvalHeldBatchIds.has(batchRunId)).map(batchRunId => {
802
1079
  const projectId = [...admittedEntries.values()].find(entry => entry.batchRunId === batchRunId)?.projectId
803
1080
  ?? [...processes.values()].find(record => record.batchRunId === batchRunId)?.projectId
804
1081
  ?? queuedEntries.find(entry => entry.batchRunId === batchRunId)?.projectId;
@@ -807,7 +1084,9 @@ export const shutdownCheckRunner = async () => {
807
1084
  }
808
1085
  finally {
809
1086
  for (const batchRunId of batchIds) {
810
- forcedBatchOutcomes.delete(batchRunId);
1087
+ for (const attempt of durableAttempts.filter(candidate => candidate.batch_run_id === batchRunId)) {
1088
+ forcedAttemptOutcomes.delete(attempt.attempt_run_id);
1089
+ }
811
1090
  }
812
1091
  }
813
1092
  };