@omnitron-dev/omnitron 0.3.5 → 0.4.6

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 (267) hide show
  1. package/README.md +296 -6
  2. package/bin/{pnpmfile-sdk.mjs → pnpmfile-sdk.js} +13 -0
  3. package/bin/sdk.d.ts +19 -0
  4. package/bin/sdk.js +932 -0
  5. package/dist/BUILD.json +2 -2
  6. package/dist/cli/omnitron.js +14 -6
  7. package/dist/cli/omnitron.js.map +1 -1
  8. package/dist/commands/stack.d.ts +2 -5
  9. package/dist/commands/stack.d.ts.map +1 -1
  10. package/dist/commands/stack.js +32 -0
  11. package/dist/commands/stack.js.map +1 -1
  12. package/dist/commands/status.d.ts.map +1 -1
  13. package/dist/commands/status.js +1 -0
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/daemon/daemon-client.d.ts +3 -15
  16. package/dist/daemon/daemon-client.d.ts.map +1 -1
  17. package/dist/daemon/daemon-client.js +4 -0
  18. package/dist/daemon/daemon-client.js.map +1 -1
  19. package/dist/daemon/daemon.d.ts +5 -11
  20. package/dist/daemon/daemon.d.ts.map +1 -1
  21. package/dist/daemon/daemon.js +28 -42
  22. package/dist/daemon/daemon.js.map +1 -1
  23. package/dist/daemon/daemon.rpc-service.d.ts +3 -15
  24. package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
  25. package/dist/daemon/daemon.rpc-service.js +27 -3
  26. package/dist/daemon/daemon.rpc-service.js.map +1 -1
  27. package/dist/daemon/darwin-owner-evidence.js +1 -1
  28. package/dist/daemon/darwin-owner-evidence.js.map +1 -1
  29. package/dist/daemon/execution-capabilities.d.ts +1 -1
  30. package/dist/daemon/execution-capabilities.d.ts.map +1 -1
  31. package/dist/daemon/execution-capabilities.js +3 -3
  32. package/dist/daemon/execution-capabilities.js.map +1 -1
  33. package/dist/daemon/execution-client.d.ts +4 -0
  34. package/dist/daemon/execution-client.d.ts.map +1 -1
  35. package/dist/daemon/execution-client.js +28 -6
  36. package/dist/daemon/execution-client.js.map +1 -1
  37. package/dist/daemon/execution-host.d.ts +4 -11
  38. package/dist/daemon/execution-host.d.ts.map +1 -1
  39. package/dist/daemon/execution-host.js +3 -9
  40. package/dist/daemon/execution-host.js.map +1 -1
  41. package/dist/daemon/execution-owner.d.ts.map +1 -1
  42. package/dist/daemon/execution-owner.js +2 -16
  43. package/dist/daemon/execution-owner.js.map +1 -1
  44. package/dist/daemon/execution-shutdown.d.ts +1 -1
  45. package/dist/daemon/execution-shutdown.d.ts.map +1 -1
  46. package/dist/daemon/managed-owner-build.d.ts +4 -0
  47. package/dist/daemon/managed-owner-build.d.ts.map +1 -1
  48. package/dist/daemon/managed-owner-build.js +14 -2
  49. package/dist/daemon/managed-owner-build.js.map +1 -1
  50. package/dist/daemon/managed-runtime.js +1 -1
  51. package/dist/daemon/managed-runtime.js.map +1 -1
  52. package/dist/daemon/owner-reconciliation.d.ts.map +1 -1
  53. package/dist/daemon/owner-reconciliation.js +1 -2
  54. package/dist/daemon/owner-reconciliation.js.map +1 -1
  55. package/dist/daemon/system-guardian-cgroup.js +1 -1
  56. package/dist/daemon/system-guardian-cgroup.js.map +1 -1
  57. package/dist/infrastructure/existing-containers.d.ts +25 -0
  58. package/dist/infrastructure/existing-containers.d.ts.map +1 -0
  59. package/dist/infrastructure/existing-containers.js +183 -0
  60. package/dist/infrastructure/existing-containers.js.map +1 -0
  61. package/dist/infrastructure/infrastructure.service.d.ts +14 -1
  62. package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
  63. package/dist/infrastructure/infrastructure.service.js +118 -9
  64. package/dist/infrastructure/infrastructure.service.js.map +1 -1
  65. package/dist/infrastructure/stack-infra-manager.d.ts +8 -2
  66. package/dist/infrastructure/stack-infra-manager.d.ts.map +1 -1
  67. package/dist/infrastructure/stack-infra-manager.js +46 -9
  68. package/dist/infrastructure/stack-infra-manager.js.map +1 -1
  69. package/dist/monitoring/log-journal.d.ts.map +1 -1
  70. package/dist/monitoring/log-journal.js +1 -2
  71. package/dist/monitoring/log-journal.js.map +1 -1
  72. package/dist/operations.d.ts +1 -0
  73. package/dist/operations.d.ts.map +1 -1
  74. package/dist/operations.js +1 -0
  75. package/dist/operations.js.map +1 -1
  76. package/dist/orchestrator/bootstrap-loader.d.ts +2 -0
  77. package/dist/orchestrator/bootstrap-loader.d.ts.map +1 -1
  78. package/dist/orchestrator/bootstrap-loader.js +2 -1
  79. package/dist/orchestrator/bootstrap-loader.js.map +1 -1
  80. package/dist/orchestrator/classic-launcher.d.ts.map +1 -1
  81. package/dist/orchestrator/classic-launcher.js +2 -1
  82. package/dist/orchestrator/classic-launcher.js.map +1 -1
  83. package/dist/orchestrator/execution-contract.d.ts +2 -3
  84. package/dist/orchestrator/execution-contract.d.ts.map +1 -1
  85. package/dist/orchestrator/file-watcher.d.ts +11 -23
  86. package/dist/orchestrator/file-watcher.d.ts.map +1 -1
  87. package/dist/orchestrator/file-watcher.js +57 -42
  88. package/dist/orchestrator/file-watcher.js.map +1 -1
  89. package/dist/orchestrator/orchestrator.service.d.ts +10 -5
  90. package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
  91. package/dist/orchestrator/orchestrator.service.js +117 -20
  92. package/dist/orchestrator/orchestrator.service.js.map +1 -1
  93. package/dist/project/bootstrap-metadata.d.ts +1 -1
  94. package/dist/project/bootstrap-metadata.d.ts.map +1 -1
  95. package/dist/project/bootstrap-metadata.js +3 -2
  96. package/dist/project/bootstrap-metadata.js.map +1 -1
  97. package/dist/project/node-stack-stop.d.ts +2 -1
  98. package/dist/project/node-stack-stop.d.ts.map +1 -1
  99. package/dist/project/node-stack-stop.js +5 -1
  100. package/dist/project/node-stack-stop.js.map +1 -1
  101. package/dist/release/closure-library-path.d.ts +8 -2
  102. package/dist/release/closure-library-path.d.ts.map +1 -1
  103. package/dist/release/closure-library-path.js +72 -28
  104. package/dist/release/closure-library-path.js.map +1 -1
  105. package/dist/release/local-installation.d.ts +2 -0
  106. package/dist/release/local-installation.d.ts.map +1 -1
  107. package/dist/release/local-installation.js +1 -1
  108. package/dist/release/local-installation.js.map +1 -1
  109. package/dist/release/portable-archive.d.ts +3 -0
  110. package/dist/release/portable-archive.d.ts.map +1 -1
  111. package/dist/release/portable-archive.js +13 -7
  112. package/dist/release/portable-archive.js.map +1 -1
  113. package/dist/release/registry-framework.d.ts +25 -0
  114. package/dist/release/registry-framework.d.ts.map +1 -1
  115. package/dist/release/registry-framework.js +51 -3
  116. package/dist/release/registry-framework.js.map +1 -1
  117. package/dist/release/target-runtime.d.ts +4 -0
  118. package/dist/release/target-runtime.d.ts.map +1 -1
  119. package/dist/release/target-runtime.js +9 -0
  120. package/dist/release/target-runtime.js.map +1 -1
  121. package/dist/services/host-upgrade.d.ts +2 -0
  122. package/dist/services/host-upgrade.d.ts.map +1 -1
  123. package/dist/services/host-upgrade.js +6 -2
  124. package/dist/services/host-upgrade.js.map +1 -1
  125. package/dist/services/infrastructure.rpc-service.d.ts +8 -0
  126. package/dist/services/infrastructure.rpc-service.d.ts.map +1 -1
  127. package/dist/services/infrastructure.rpc-service.js +90 -10
  128. package/dist/services/infrastructure.rpc-service.js.map +1 -1
  129. package/dist/services/kernel-rollout.service.d.ts.map +1 -1
  130. package/dist/services/kernel-rollout.service.js +1 -2
  131. package/dist/services/kernel-rollout.service.js.map +1 -1
  132. package/dist/services/migration-process.d.ts +1 -0
  133. package/dist/services/migration-process.d.ts.map +1 -1
  134. package/dist/services/migration-process.js +4 -2
  135. package/dist/services/migration-process.js.map +1 -1
  136. package/dist/services/pipeline-definition.d.ts.map +1 -1
  137. package/dist/services/pipeline-definition.js +32 -6
  138. package/dist/services/pipeline-definition.js.map +1 -1
  139. package/dist/services/pipeline.service.d.ts +7 -1
  140. package/dist/services/pipeline.service.d.ts.map +1 -1
  141. package/dist/services/pipeline.service.js +106 -13
  142. package/dist/services/pipeline.service.js.map +1 -1
  143. package/dist/services/project.rpc-service.d.ts +6 -5
  144. package/dist/services/project.rpc-service.d.ts.map +1 -1
  145. package/dist/services/project.rpc-service.js +13 -0
  146. package/dist/services/project.rpc-service.js.map +1 -1
  147. package/dist/services/project.service.d.ts +24 -6
  148. package/dist/services/project.service.d.ts.map +1 -1
  149. package/dist/services/project.service.js +327 -59
  150. package/dist/services/project.service.js.map +1 -1
  151. package/dist/services/remote-deployer.service.d.ts.map +1 -1
  152. package/dist/services/remote-deployer.service.js +3 -0
  153. package/dist/services/remote-deployer.service.js.map +1 -1
  154. package/dist/shared/dto/backend.d.ts +3 -0
  155. package/dist/shared/dto/backend.d.ts.map +1 -1
  156. package/dist/shared/dto/backend.js.map +1 -1
  157. package/dist/shared/dto/pipelines.d.ts +54 -5
  158. package/dist/shared/dto/pipelines.d.ts.map +1 -1
  159. package/dist/shared/dto/pipelines.js +0 -7
  160. package/dist/shared/dto/pipelines.js.map +1 -1
  161. package/dist/shared/dto/project.d.ts +60 -0
  162. package/dist/shared/dto/project.d.ts.map +1 -1
  163. package/dist/shared/dto/services.d.ts +26 -17
  164. package/dist/shared/dto/services.d.ts.map +1 -1
  165. package/dist/shared/dto/services.js.map +1 -1
  166. package/dist/workers/system-worker-manager.d.ts +1 -2
  167. package/dist/workers/system-worker-manager.d.ts.map +1 -1
  168. package/dist/workers/system-worker-manager.js +5 -3
  169. package/dist/workers/system-worker-manager.js.map +1 -1
  170. package/package.json +17 -16
  171. package/webapp/dist/BUILD.json +2 -2
  172. package/webapp/dist/assets/{access-DUjabwNX.js → access-B2gXuoMG.js} +1 -1
  173. package/webapp/dist/assets/{access-BvKK9dEv.js → access-I0f72P9m.js} +1 -1
  174. package/webapp/dist/assets/{admin-data-table-BKSDI8sU.js → admin-data-table-CAEL2f-m.js} +1 -1
  175. package/webapp/dist/assets/{alert-uBG6r9NW.js → alert-BdJ2uaY0.js} +1 -1
  176. package/webapp/dist/assets/{alerts-CzN6OqX5.js → alerts-DeVkMXWo.js} +1 -1
  177. package/webapp/dist/assets/{apps-G_Lm6m4F.js → apps-9SuDvyGQ.js} +1 -1
  178. package/webapp/dist/assets/{audit-CLMpdBzG.js → audit-DT8cY7Ys.js} +1 -1
  179. package/webapp/dist/assets/backend-status.store-BbBE9cn8.js +4 -0
  180. package/webapp/dist/assets/{card-DkH32gMY.js → card-DaUEmUI5.js} +1 -1
  181. package/webapp/dist/assets/{chart-BpGXlCJc.js → chart-BKXP2ter.js} +1 -1
  182. package/webapp/dist/assets/{client-DFnuv2z3.js → client-CgyTwq9g.js} +1 -1
  183. package/webapp/dist/assets/{code-editor-DK6Tmmxv.js → code-editor-JVuQ4m-z.js} +1 -1
  184. package/webapp/dist/assets/{comparison-bars-BtC0dHjF.js → comparison-bars-Ds5Dt1oj.js} +1 -1
  185. package/webapp/dist/assets/{confirm-dialog-BbGsAz7_.js → confirm-dialog-BayExVbF.js} +1 -1
  186. package/webapp/dist/assets/{console-keyboard-D3YCA0yU.js → console-keyboard-uSLt92dB.js} +1 -1
  187. package/webapp/dist/assets/{containers-BsFwprq4.js → containers-DkXTgcrY.js} +1 -1
  188. package/webapp/dist/assets/{controls-C5W8gHU4.js → controls-CqeGPnaF.js} +1 -1
  189. package/webapp/dist/assets/{dashboard-BN7rKE5n.js → dashboard-By5yPuOf.js} +1 -1
  190. package/webapp/dist/assets/{dashboard-builder-BXFxWjXP.js → dashboard-builder-C0hvBwkd.js} +1 -1
  191. package/webapp/dist/assets/{deploy-progress-CUUwlYtQ.js → deploy-progress-B39cvvCE.js} +1 -1
  192. package/webapp/dist/assets/{deploy-release-dialog-58lGZDNk.js → deploy-release-dialog-CnzaO_xL.js} +1 -1
  193. package/webapp/dist/assets/{deployments-DEzjHerg.js → deployments-BXP943hA.js} +1 -1
  194. package/webapp/dist/assets/{detail-pXPk4O0E.js → detail-B84RgiOb.js} +1 -1
  195. package/webapp/dist/assets/{detail-Dj3gxaH0.js → detail-CVXDks9-.js} +1 -1
  196. package/webapp/dist/assets/{detail-CBn9pjJU.js → detail-D9DixtaK.js} +1 -1
  197. package/webapp/dist/assets/{empty-content-7jOFhG4C.js → empty-content-eyDOuOVp.js} +1 -1
  198. package/webapp/dist/assets/{error-boundary-CLLI8PWw.js → error-boundary-CQmDr_HS.js} +1 -1
  199. package/webapp/dist/assets/{event-fields-CiyqrRO8.js → event-fields-C7p2a2HY.js} +1 -1
  200. package/webapp/dist/assets/{files-DigwXndm.js → files-BsfGT3t6.js} +2 -2
  201. package/webapp/dist/assets/{icons-OboZgWSt.js → icons-B9Ct8PJ9.js} +1 -1
  202. package/webapp/dist/assets/index-BEfv96EF.js +1 -0
  203. package/webapp/dist/assets/{label-B1SF0RTL.js → label-avMhe08g.js} +1 -1
  204. package/webapp/dist/assets/{log-level-filter-Bcn31D1F.js → log-level-filter-oaUGdbpk.js} +1 -1
  205. package/webapp/dist/assets/{logs-DrsBbLfb.js → logs-iC1OEDv3.js} +1 -1
  206. package/webapp/dist/assets/{metric-chart-M-kI-XOt.js → metric-chart-CSa_qv4F.js} +1 -1
  207. package/webapp/dist/assets/{metrics-D5cDZTEo.js → metrics-CKCmcGKw.js} +1 -1
  208. package/webapp/dist/assets/{mui-Cd711nOk.js → mui-B3fJdIMc.js} +1 -1
  209. package/webapp/dist/assets/{mui-CdB_p2lt.js → mui-Bdr6Ks5j.js} +1 -1
  210. package/webapp/dist/assets/{mui-Db5J-QUd.js → mui-CADnFGCx.js} +1 -1
  211. package/webapp/dist/assets/{mui-AnEgAVjz.js → mui-CMWGTMXv.js} +1 -1
  212. package/webapp/dist/assets/{mui-DyT2n1lx.js → mui-CN3dT7bb.js} +1 -1
  213. package/webapp/dist/assets/{mui-BwsIrivo.js → mui-CPEkFAPo.js} +1 -1
  214. package/webapp/dist/assets/{mui-dLCwWoMw.js → mui-D3qAWvfZ.js} +1 -1
  215. package/webapp/dist/assets/{mui-D5m7fDCh.js → mui-D7tBU80e.js} +1 -1
  216. package/webapp/dist/assets/{mui-59N0OVRa.js → mui-DaJavAoc.js} +1 -1
  217. package/webapp/dist/assets/{mui-BXSXx9c9.js → mui-SNRRJ_vi.js} +1 -1
  218. package/webapp/dist/assets/{nodes-CwwZZuu4.js → nodes-DtLyXH0V.js} +1 -1
  219. package/webapp/dist/assets/{operation-status-Bm1x1wTz.js → operation-status-DHyYOT7G.js} +1 -1
  220. package/webapp/dist/assets/{page-breadcrumbs-fR7Jmpp4.js → page-breadcrumbs-3R8RZ3nA.js} +1 -1
  221. package/webapp/dist/assets/pipelines-Do2NAAqK.js +3 -0
  222. package/webapp/dist/assets/{project.store-DuCgv6Eq.js → project.store-D5IR3fo9.js} +1 -1
  223. package/webapp/dist/assets/{projects-TBAdpCeX.js → projects-BqlGbzeG.js} +1 -1
  224. package/webapp/dist/assets/{realtime.store-hfxZD2DU.js → realtime.store-C8D235-B.js} +1 -1
  225. package/webapp/dist/assets/{release-bits-BJ6i23y0.js → release-bits-IehuBssK.js} +1 -1
  226. package/webapp/dist/assets/{release-build-panel-BNXkwNE1.js → release-build-panel-C48hP_9w.js} +1 -1
  227. package/webapp/dist/assets/{release-deployment-state-gN8ZT3aq.js → release-deployment-state-BfmxZP4Y.js} +1 -1
  228. package/webapp/dist/assets/{releases-CXXKUh-v.js → releases-Cm0RV5Xu.js} +1 -1
  229. package/webapp/dist/assets/{search-input-C0MHI8vO.js → search-input-SWD1dXxD.js} +1 -1
  230. package/webapp/dist/assets/settings-DwuEPQGg.js +1 -0
  231. package/webapp/dist/assets/sign-in-CQ-WeS5A.js +1 -0
  232. package/webapp/dist/assets/{skeleton-BmWDJWF9.js → skeleton-BnVxAoeX.js} +1 -1
  233. package/webapp/dist/assets/{snackbar-DSPmjMyi.js → snackbar-CRX6eqRT.js} +1 -1
  234. package/webapp/dist/assets/{stack-health-status-BIv2C10x.js → stack-health-status-DfErRNb1.js} +1 -1
  235. package/webapp/dist/assets/{stack-start-notice-D-AH_bzn.js → stack-start-notice-NkYDjR-f.js} +1 -1
  236. package/webapp/dist/assets/{stacks-WY_ynkON.js → stacks-BOGCfW90.js} +1 -1
  237. package/webapp/dist/assets/{store-bqUBn2A4.js → store-DtTtbc9H.js} +1 -1
  238. package/webapp/dist/assets/{system-info-DXMwo6gl.js → system-info-Cw-xywgI.js} +1 -1
  239. package/webapp/dist/assets/{table-BOWT6Y9d.js → table-D-so_B1p.js} +1 -1
  240. package/webapp/dist/assets/{table-viewport-Bp4pq-c_.js → table-viewport-DdZ1yAKm.js} +1 -1
  241. package/webapp/dist/assets/{tabs-k5NPGLnJ.js → tabs-DGqBkXh_.js} +1 -1
  242. package/webapp/dist/assets/{terminal-QWBau-Us.js → terminal-Bbl0XNeB.js} +1 -1
  243. package/webapp/dist/assets/{topology-C_Qp1RqW.js → topology-DmrRy7nN.js} +1 -1
  244. package/webapp/dist/assets/{topology-metadata-dialog-BivuU2y6.js → topology-metadata-dialog-Dpfp4YF1.js} +1 -1
  245. package/webapp/dist/assets/{traces-By0rdfDC.js → traces-CKHsOabJ.js} +1 -1
  246. package/webapp/dist/assets/{use-stack-context-8yaHaN8L.js → use-stack-context-BuraYaeY.js} +1 -1
  247. package/webapp/dist/assets/webapp-D8h_eabt.js +35 -0
  248. package/webapp/dist/console-version.json +1 -1
  249. package/webapp/dist/index.html +34 -34
  250. package/webapp/dist/logo.svg +1 -80
  251. package/bin/sdk.d.mts +0 -8
  252. package/bin/sdk.mjs +0 -580
  253. package/dist/monitoring/process-lifetime.d.ts +0 -11
  254. package/dist/monitoring/process-lifetime.d.ts.map +0 -1
  255. package/dist/monitoring/process-lifetime.js +0 -45
  256. package/dist/monitoring/process-lifetime.js.map +0 -1
  257. package/dist/shared/host-boot-identity.d.ts +0 -12
  258. package/dist/shared/host-boot-identity.d.ts.map +0 -1
  259. package/dist/shared/host-boot-identity.js +0 -89
  260. package/dist/shared/host-boot-identity.js.map +0 -1
  261. package/webapp/dist/assets/index-Bo9An3pJ.js +0 -1
  262. package/webapp/dist/assets/omnitron-logo-koMCbsCv.js +0 -1
  263. package/webapp/dist/assets/pipelines-BrfTjf6Y.js +0 -3
  264. package/webapp/dist/assets/settings-B3egRvbI.js +0 -1
  265. package/webapp/dist/assets/sign-in-9q-Nfy83.js +0 -1
  266. package/webapp/dist/assets/webapp-BO_WoB_z.js +0 -35
  267. package/webapp/dist/favicon.png +0 -0
@@ -11,6 +11,9 @@ import { readProjectStacks, resolveProjectStacks } from '../config/project-stack
11
11
  import { readSavedDaemonConfig } from '../config/saved.js';
12
12
  import { observeStackHealth } from '../project/stack-health.js';
13
13
  import { LocalReleaseSelection } from '../project/local-release-selection.js';
14
+ import { assertResidentApplicationRuntime, assertSameApplicationRuntime } from './host-upgrade.js';
15
+ import { assertReleaseLoaderEnvironment, assertReleaseTarget, releaseOptionsForEnv } from '../release/target-runtime.js';
16
+ import { withClosureLibraryPath } from '../release/closure-library-path.js';
14
17
  import { resolveAppPath } from '../config/app-path.js';
15
18
  import { infrastructureOwnership, ownsStackInfrastructure } from '../project/infrastructure-ownership.js';
16
19
  import { InstalledStackResume, resumeDestinations } from '../project/installed-stack-resume.js';
@@ -18,7 +21,7 @@ import { isDeepStrictEqual } from 'node:util';
18
21
  import { diagnoseContainers } from '../project/stack-diagnosis.js';
19
22
  import { diagnosisStatus } from '../shared/dto/stack-diagnosis.js';
20
23
  import { buildInfraFromRequirements, requirementsFromDefinitions } from '../project/requirements-scanner.js';
21
- import { randomUUID } from 'node:crypto';
24
+ import { randomUUID, createHash } from 'node:crypto';
22
25
  import { NodeStackStop, stackDefinitionFingerprint } from '../project/node-stack-stop.js';
23
26
  import { stackStopReview } from '../project/stack-stop-review.js';
24
27
  import { inspectStackContainers, removeContainer } from '../infrastructure/container-runtime.js';
@@ -272,7 +275,18 @@ export class ProjectService extends EventEmitter {
272
275
  execution: orchestrator,
273
276
  definition: (scope, cached) => this.stopDefinition(scope, cached),
274
277
  containers: scope => inspectStackContainers(scope),
275
- disableResume: scope => this.registry.setStackEnabled(scope.project, scope.stack, false),
278
+ disableResume: (scope, keep) => {
279
+ if (keep)
280
+ this.setInfrastructurePolicy(scope.project, scope.stack, null);
281
+ this.registry.setStackEnabled(scope.project, scope.stack, false);
282
+ },
283
+ holdInfrastructure: async (scope) => {
284
+ await this.drainHostedProvisioning(scope);
285
+ const hosted = this.hostedInfrastructure();
286
+ if (hosted?.representsStack(scope))
287
+ await hosted.holdForPreservation();
288
+ await this.infraManager.getInstance(scope.project, scope.stack)?.holdForPreservation();
289
+ },
276
290
  retireInfrastructure: async (scope) => {
277
291
  await this.drainHostedProvisioning(scope);
278
292
  await this.hostedInfrastructure()?.retireStack(scope);
@@ -1317,15 +1331,15 @@ export class ProjectService extends EventEmitter {
1317
1331
  continue;
1318
1332
  for (const app of await this.launchEntries(scope.project, name, stack, config))
1319
1333
  if (app.bootstrap)
1320
- bootstraps.set(resolveAppPath(directory, app, app.bootstrap), app.name);
1334
+ bootstraps.set(resolveAppPath(directory, app, app.bootstrap), { app: app.name, ...(app.releaseRoot && { releaseRoot: app.releaseRoot }) });
1321
1335
  }
1322
1336
  const result = { refreshed: 0, failures: [] };
1323
- for (const [bootstrap, app] of bootstraps) {
1337
+ for (const [bootstrap, { app, releaseRoot }] of bootstraps) {
1324
1338
  try {
1325
1339
  const remaining = Math.min(30_000, deadline - Date.now());
1326
1340
  if (remaining <= 0)
1327
1341
  throw new Error('Declaration refresh deadline exceeded; this application was not read');
1328
- const { metadata: record } = await readBootstrapMetadata(bootstrap, false, remaining);
1342
+ const { metadata: record } = await readBootstrapMetadata(bootstrap, false, remaining, releaseRoot);
1329
1343
  this.bootstrapMetadata.save(record);
1330
1344
  this.bootstrapTopology.set(bootstrap, record.definition);
1331
1345
  result.refreshed += 1;
@@ -1385,6 +1399,109 @@ export class ProjectService extends EventEmitter {
1385
1399
  */
1386
1400
  rereading;
1387
1401
  startsInFlight = new Map();
1402
+ infrastructurePolicyKey(project, stack) {
1403
+ const registration = this.registry.get(project);
1404
+ if (!registration?.registeredAt)
1405
+ throw Error('Infrastructure authority requires a project registration');
1406
+ return `lifecycle:infrastructure:${stackExecutionKey({ project, stack })}:${registration.registeredAt}`;
1407
+ }
1408
+ infrastructurePolicy(project, stack) {
1409
+ const value = this.daemonStateStore.kvGetSync(this.infrastructurePolicyKey(project, stack), { strict: true });
1410
+ if (value && (value.mode !== 'preserve' || (value.review !== null && !/^[a-f0-9]{64}$/.test(value.review))))
1411
+ throw Error('Invalid retained infrastructure authority');
1412
+ return value;
1413
+ }
1414
+ setInfrastructurePolicy(project, stack, review, reconcile = false) {
1415
+ this.daemonStateStore.kvUpdateSync(this.infrastructurePolicyKey(project, stack), () => reconcile ? null : { mode: 'preserve', review });
1416
+ }
1417
+ /** Read declarations without importing application code or executing startup hooks. */
1418
+ preservationDefinitions(project, stack, entries) {
1419
+ const root = this.registry.get(project)?.path;
1420
+ if (!root)
1421
+ throw Error('Project is not registered');
1422
+ const definitions = new Map();
1423
+ for (const entry of entries) {
1424
+ if (entry.releaseFrom)
1425
+ throw Error('Preservation requires a prepared release installation');
1426
+ if (!entry.bootstrap)
1427
+ continue;
1428
+ const bootstrap = resolveAppPath(root, entry, entry.bootstrap);
1429
+ const { metadata, declared, state, record } = this.readAppDeclaration(project, stack, entry.name, bootstrap);
1430
+ if (declared.malformed || !metadata || !record || state?.failure || metadataSourceChanged(record))
1431
+ throw Error('Current application declarations must be captured before reviewing infrastructure preservation');
1432
+ definitions.set(entry.name, withDeclaredConfig({ name: entry.name, version: '', processes: [], ...metadata }, declared));
1433
+ }
1434
+ return definitions;
1435
+ }
1436
+ /** Shared lifecycle preflight. No release installation, bootstrap import, provisioning or database effects. */
1437
+ async reviewStackStart(project, stackName, options = {}) {
1438
+ if (options.infrastructure !== undefined && options.infrastructure !== 'preserve')
1439
+ throw Error('Start review requires infrastructure=preserve');
1440
+ if (options.workingTree && options.release)
1441
+ throw Error('Select either a release or the working tree');
1442
+ const report = { protocol: 1, project, stack: stackName, infrastructure: 'preserve', checkedAt: new Date().toISOString(),
1443
+ digest: null, targets: [], deferred: ['Application startup and application health; framework setup and migrations are forbidden in preserve mode'] };
1444
+ try {
1445
+ const config = this.getLoadedConfig(project);
1446
+ if (!config)
1447
+ throw Error('Project configuration is not loaded');
1448
+ const stack = this.resolveStacks(config, project)[stackName];
1449
+ if (!stack)
1450
+ throw Error('Stack is not declared');
1451
+ this.assertStartAuthority({ project, stack: stackName }, stack);
1452
+ const local = stack.type === 'local' && !stack.managedOrigin;
1453
+ const prior = local ? this.localSelection(project, stackName).read().release : null;
1454
+ const id = options.release ?? (options.workingTree ? undefined : prior?.id);
1455
+ report.selection = id ? { release: id } : { workingTree: true };
1456
+ const release = id ? await this.admitRelease(project, stackName, stack, config, id) : null;
1457
+ if (!release && stack.release?.mode === 'required')
1458
+ throw Error('A verified release is required');
1459
+ if (release && prior && prior.id === id && prior.manifestSha256 !== release.manifestSha256)
1460
+ throw Error('Selected release changed');
1461
+ const installation = local && release ? await this.installedRelease(project, stackName, stack, config, release, false) : undefined;
1462
+ const entries = installation?.entries ?? this.resolveStackApps(stack, config);
1463
+ resolveStartupOrder(entries);
1464
+ const definitions = release?.remoteDefinitions ?? this.preservationDefinitions(project, stackName, entries);
1465
+ const environment = await this.stackEnvFor(project, stackName, stack, entries.map(entry => entry.name));
1466
+ const effective = mergeInferredInfrastructure(buildInfraFromRequirements(requirementsFromDefinitions(definitions)), installation?.infrastructure ?? mergeInfrastructure(config, stack));
1467
+ const normalized = normalizeInfraConfig(effective ?? {}, createDefaultRegistry());
1468
+ const requirements = { ...normalized };
1469
+ for (const definition of definitions.values())
1470
+ Object.assign(requirements, definition.omnitronConfig?.infrastructure);
1471
+ if (Object.entries(requirements).some(([name, requirement]) => {
1472
+ const binding = bindService(requirement, stack.serviceOverrides?.[name]);
1473
+ return binding.provisioning === 'bareMetal' || binding.provisioning === 'none';
1474
+ }))
1475
+ throw Error('This infrastructure resource has no verifiable native preservation identity');
1476
+ if (stack.type === 'local') {
1477
+ const infrastructure = await this.infraManager.reviewPreservation(project, stackName, stack, effective ?? {}, definitions, this.registry.get(project).path);
1478
+ report.targets.push({ node: 'local', infrastructure });
1479
+ }
1480
+ else {
1481
+ const services = await this.collectDeclaredServices(project, stack, config, { definitions });
1482
+ for (const { node } of resolveNodePlacements(stack, entries)) {
1483
+ const infrastructure = await this.reviewNodeInfrastructure(node, mergeInfrastructure(config, stack) ?? {}, services, { project, stack: stackName, overrides: await this.resolveOverrideSecrets(project, stack.serviceOverrides) }, this.registry.get(project).path, release?.staticsDir ?? undefined);
1484
+ report.targets.push({ node: stackNodeKey(node.host, node.port ?? 9700), infrastructure });
1485
+ }
1486
+ }
1487
+ // Bind launch bytes as well as declarations; release installation verification binds the full payload closure.
1488
+ const launch = stack.type === 'local' ? entries.map(entry => {
1489
+ const script = entry.bootstrap ?? entry.script;
1490
+ return script ? [entry.name, createHash('sha256').update(fs.readFileSync(resolveAppPath(this.registry.get(project).path, entry, script))).digest('hex')] : [entry.name];
1491
+ }) : [];
1492
+ if (report.targets.length && report.targets.every(target => target.infrastructure?.digest))
1493
+ report.digest = stackDefinitionFingerprint([
1494
+ 'omnitron/stack-start/preserve/v1', { project, stack: stackName }, this.registry.get(project).registeredAt,
1495
+ stack, entries, [...definitions], environment, release?.manifestSha256 ?? null, launch,
1496
+ report.targets.map(target => [target.node, target.infrastructure.digest]),
1497
+ ]);
1498
+ }
1499
+ catch {
1500
+ // Source errors may contain credential values. Evidence contains only reviewed fingerprints and controlled diagnostics.
1501
+ report.targets.push({ node: 'preflight', reason: 'Complete launch and infrastructure preservation could not be verified. Check release preparation, current declarations and recipient capability.' });
1502
+ }
1503
+ return report;
1504
+ }
1388
1505
  async startStack(projectName, stackName, opts) {
1389
1506
  if (this.resumingInstalled)
1390
1507
  throw new Error('An installed allocation is resuming');
@@ -1397,7 +1514,19 @@ export class ProjectService extends EventEmitter {
1397
1514
  throw new Error('Project definitions are being updated; retry the start');
1398
1515
  if (opts?.workingTree && opts.release)
1399
1516
  throw new Error('Select either a release or the working tree');
1400
- const selection = JSON.stringify([opts?.release, opts?.workingTree === true, opts?.reinstall === true]);
1517
+ if (opts?.infrastructure !== undefined && !['reconcile', 'preserve'].includes(opts.infrastructure))
1518
+ throw Error('Invalid infrastructure start mode');
1519
+ const policy = this.infrastructurePolicy(projectName, stackName);
1520
+ if (policy && opts?.infrastructure === 'reconcile' && (opts.source === 'boot' || opts.source === 'auto-resume'))
1521
+ throw Error('Automatic starts cannot expand preserved infrastructure authority');
1522
+ const mode = opts?.infrastructure ?? policy?.mode ?? 'reconcile';
1523
+ const review = opts?.review ?? (mode === 'preserve' ? policy?.review : undefined);
1524
+ if (mode === 'preserve' && (!review || !/^[a-f0-9]{64}$/.test(review)))
1525
+ throw Error('Preserved infrastructure requires a successful stack start review; inspect --dry-run --infrastructure preserve');
1526
+ if (opts?.review && mode !== 'preserve')
1527
+ throw Error('A preservation review cannot authorize reconciliation');
1528
+ const infrastructure = { mode, review: review ?? undefined, explicitReconcile: opts?.infrastructure === 'reconcile' };
1529
+ const selection = JSON.stringify([opts?.release, opts?.workingTree === true, opts?.reinstall === true, infrastructure]);
1401
1530
  const running = this.startsInFlight.get(inFlightKey);
1402
1531
  if (running) {
1403
1532
  this.logger.info({ project: projectName, stack: stackName }, 'This stack is already being started — joining the run in progress');
@@ -1405,7 +1534,7 @@ export class ProjectService extends EventEmitter {
1405
1534
  throw new Error('Another stack start selected different launch code; retry after it completes');
1406
1535
  return running.promise;
1407
1536
  }
1408
- const started = this.startStackOnce(projectName, stackName, opts?.source ?? 'unknown', opts?.allowDirty === true, opts?.release, opts?.reinstall === true, opts?.workingTree === true).finally(() => {
1537
+ const started = this.startStackOnce(projectName, stackName, opts?.source ?? 'unknown', opts?.allowDirty === true, opts?.release, opts?.reinstall === true, opts?.workingTree === true, infrastructure).finally(() => {
1409
1538
  this.startsInFlight.delete(inFlightKey);
1410
1539
  });
1411
1540
  this.startsInFlight.set(inFlightKey, { selection, promise: started });
@@ -1436,10 +1565,10 @@ export class ProjectService extends EventEmitter {
1436
1565
  * deploys nothing, and the reconciler re-asks a node that does not answer
1437
1566
  * every few minutes — rows about looking, not about changing anything.
1438
1567
  */
1439
- async startStackOnce(projectName, stackName, source, allowDirty, releaseId, reinstall = false, workingTree = false) {
1568
+ async startStackOnce(projectName, stackName, source, allowDirty, releaseId, reinstall = false, workingTree = false, infrastructure) {
1440
1569
  const known = { attach: false };
1441
1570
  try {
1442
- return await this.runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall, workingTree);
1571
+ return await this.runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall, workingTree, infrastructure);
1443
1572
  }
1444
1573
  catch (err) {
1445
1574
  if (!known.attach) {
@@ -1451,6 +1580,8 @@ export class ProjectService extends EventEmitter {
1451
1580
  error: err,
1452
1581
  details: {
1453
1582
  source,
1583
+ ...(infrastructure?.mode === 'preserve' ? { infrastructureMode: 'preserve', review: infrastructure.review }
1584
+ : infrastructure?.explicitReconcile ? { infrastructureMode: 'reconcile' } : {}),
1454
1585
  ...(known.type ? { type: known.type } : {}),
1455
1586
  ...((releaseId ?? known.release) ? { release: releaseId ?? known.release } : {}),
1456
1587
  ...(known.commit !== undefined ? { commit: known.commit } : {}),
@@ -1460,8 +1591,10 @@ export class ProjectService extends EventEmitter {
1460
1591
  throw err;
1461
1592
  }
1462
1593
  }
1463
- async runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall = false, workingTree = false) {
1464
- const config = await this.loadProjectConfig(projectName);
1594
+ async runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall = false, workingTree = false, infrastructure) {
1595
+ const config = infrastructure?.mode === 'preserve' ? this.getLoadedConfig(projectName) : await this.loadProjectConfig(projectName);
1596
+ if (!config)
1597
+ throw Error('Project configuration must be loaded before preservation');
1465
1598
  const stacks = this.resolveStacks(config, projectName);
1466
1599
  const stackConfig = stacks[stackName];
1467
1600
  if (!stackConfig)
@@ -1475,6 +1608,13 @@ export class ProjectService extends EventEmitter {
1475
1608
  return this.attachRemoteStack(projectName, stackName, stackConfig, source);
1476
1609
  }
1477
1610
  this.assertStartAuthority({ project: projectName, stack: stackName }, stackConfig);
1611
+ let preservation;
1612
+ if (infrastructure?.mode === 'preserve') {
1613
+ preservation = await this.reviewStackStart(projectName, stackName, { infrastructure: 'preserve', ...(releaseId && { release: releaseId }), ...(workingTree && { workingTree: true }) });
1614
+ if (!preservation.digest || preservation.digest !== infrastructure.review)
1615
+ throw Error('Stack launch or native infrastructure changed since review; no startup effects were admitted');
1616
+ this.setInfrastructurePolicy(projectName, stackName, preservation.digest);
1617
+ }
1478
1618
  // A release, when one is named — and a stack that takes nothing else.
1479
1619
  const local = stackConfig.type === 'local' && !stackConfig.managedOrigin;
1480
1620
  if ((workingTree || (releaseId && stackConfig.type === 'local')) && !local)
@@ -1568,8 +1708,8 @@ export class ProjectService extends EventEmitter {
1568
1708
  if (mounted.length && (!local || !release || !ownsStackInfrastructure(stackConfig)))
1569
1709
  throw Error('Artifact volumes require a selected release on an independently owned local stack');
1570
1710
  const installation = mounted.length && release
1571
- ? await this.installedRelease(projectName, stackName, stackConfig, config, release, true) : undefined;
1572
- const entries = installation?.entries ?? (local && release ? await this.installedEntries(projectName, stackName, stackConfig, config, release, true) : undefined);
1711
+ ? await this.installedRelease(projectName, stackName, stackConfig, config, release, !preservation) : undefined;
1712
+ const entries = installation?.entries ?? (local && release ? await this.installedEntries(projectName, stackName, stackConfig, config, release, !preservation) : undefined);
1573
1713
  let localMembershipChanged = false;
1574
1714
  if (local) {
1575
1715
  const target = entries ?? this.resolveStackApps(stackConfig, config);
@@ -1613,7 +1753,7 @@ export class ProjectService extends EventEmitter {
1613
1753
  // wrapper returns the info untouched.
1614
1754
  const current = await this.withRemoteAppStatuses(projectName, this.toStackInfo(projectName, stackName, stackConfig));
1615
1755
  const down = current.apps.filter((a) => a.status !== 'online');
1616
- if (down.length === 0 && !current.notUp?.length) {
1756
+ if (down.length === 0 && !current.notUp?.length && !preservation && !infrastructure?.explicitReconcile) {
1617
1757
  if (existing)
1618
1758
  existing.status = 'running';
1619
1759
  this.logger.warn({ project: projectName, stack: stackName }, 'Stack already running');
@@ -1661,20 +1801,22 @@ export class ProjectService extends EventEmitter {
1661
1801
  if (changing && plan.phase !== 'stopped')
1662
1802
  throw new LocalReleaseStopRequired('Artifact volume selection requires the owner stop fence');
1663
1803
  return (await this.installedRelease(projectName, stackName, stackConfig, config, release, false)).infrastructure;
1664
- } : undefined);
1804
+ } : undefined, preservation, infrastructure?.explicitReconcile);
1665
1805
  }
1666
1806
  else {
1667
- await this.startLocalStack(projectName, stackName, stackConfig, config, undefined, async () => { await enable(); });
1807
+ await this.startLocalStack(projectName, stackName, stackConfig, config, undefined, async () => { await enable(); }, undefined, undefined, preservation, infrastructure?.explicitReconcile);
1668
1808
  }
1669
1809
  }
1670
1810
  else {
1671
1811
  this.rememberStackTargets(projectName, stackName, stackConfig, config);
1672
- reach = await this.startRemoteStack(projectName, stackName, stackConfig, config, release, reinstall);
1812
+ reach = await this.startRemoteStack(projectName, stackName, stackConfig, config, release, reinstall, preservation, infrastructure?.explicitReconcile);
1673
1813
  }
1674
1814
  state.status = 'running';
1675
1815
  state.startedAt = Date.now();
1676
1816
  // Update registry with enabled stack
1677
1817
  this.updateEnabledStacks(projectName, stackName, true);
1818
+ if (infrastructure?.explicitReconcile)
1819
+ this.setInfrastructurePolicy(projectName, stackName, null, true);
1678
1820
  this.markPriorStopSuperseded(projectName, stackName);
1679
1821
  const info = this.toStackInfo(projectName, stackName, stackConfig);
1680
1822
  // What did not come up, said in the row. A boot at 12:14 UTC on
@@ -1726,6 +1868,8 @@ export class ProjectService extends EventEmitter {
1726
1868
  apps: info.apps.length,
1727
1869
  ...(notUp.length > 0 ? { notUp: notUp.join(', ') } : {}),
1728
1870
  source,
1871
+ ...(preservation ? { infrastructureMode: 'preserve', review: preservation.digest }
1872
+ : infrastructure?.explicitReconcile ? { infrastructureMode: 'reconcile' } : {}),
1729
1873
  ...(reach ? { nodes: reach.nodes, reached: reach.reached } : {}),
1730
1874
  ...(reach && reach.skipped.length > 0 ? { skipped: reach.skipped } : {}),
1731
1875
  ...(reach && (reach.notReady?.length ?? 0) > 0 ? { notReady: reach.notReady } : {}),
@@ -1966,6 +2110,8 @@ export class ProjectService extends EventEmitter {
1966
2110
  if (cluster)
1967
2111
  await cluster.election.confirmStopAuthority();
1968
2112
  this.updateEnabledStacks(projectName, stackName, false);
2113
+ if (keepInfrastructure)
2114
+ this.setInfrastructurePolicy(projectName, stackName, null);
1969
2115
  progress.phase = 'stopping';
1970
2116
  progress.intentCommitted = true;
1971
2117
  persist();
@@ -1980,13 +2126,18 @@ export class ProjectService extends EventEmitter {
1980
2126
  }
1981
2127
  }
1982
2128
  else if (localPlan && localInventory) {
1983
- await this.orchestrator.stopStackExecution({ operationId: progress.id, plan: localPlan });
1984
- if (keepInfrastructure) {
2129
+ const verifyRetained = async () => {
1985
2130
  const retained = await this.infraManager.inspectStopContainers(projectName, stackName);
1986
- if (!isDeepStrictEqual([...retained].sort((a, b) => a.id.localeCompare(b.id)), [...localInventory.containers].sort((a, b) => a.id.localeCompare(b.id)))) {
2131
+ if (!isDeepStrictEqual([...retained].sort((a, b) => a.id.localeCompare(b.id)), [...localInventory.containers].sort((a, b) => a.id.localeCompare(b.id))))
1987
2132
  throw new Error('Retained stack infrastructure changed; inspect before confirming this application stop');
1988
- }
2133
+ };
2134
+ if (keepInfrastructure) {
2135
+ await this.infraManager.getInstance(projectName, stackName)?.holdForPreservation();
2136
+ await verifyRetained();
1989
2137
  }
2138
+ await this.orchestrator.stopStackExecution({ operationId: progress.id, plan: localPlan });
2139
+ if (keepInfrastructure)
2140
+ await verifyRetained();
1990
2141
  else
1991
2142
  await this.infraManager.teardownStack(projectName, stackName, localInventory.containers);
1992
2143
  }
@@ -3298,7 +3449,7 @@ export class ProjectService extends EventEmitter {
3298
3449
  // ===========================================================================
3299
3450
  // Private — Stack Launchers
3300
3451
  // ===========================================================================
3301
- async startLocalStack(projectName, stackName, stackConfig, ecosystemConfig, selectedEntries, enable, verifyMigrations, verifyArtifactInfrastructure) {
3452
+ async startLocalStack(projectName, stackName, stackConfig, ecosystemConfig, selectedEntries, enable, verifyMigrations, verifyArtifactInfrastructure, preservation, reconcile = false) {
3302
3453
  const ownsInfrastructure = ownsStackInfrastructure(stackConfig);
3303
3454
  const stateKey = `${projectName}/${stackName}`;
3304
3455
  const state = this.stackStates.get(stateKey);
@@ -3309,7 +3460,8 @@ export class ProjectService extends EventEmitter {
3309
3460
  // start here (project/stack-env.ts).
3310
3461
  const stackEnv = await this.stackEnvFor(projectName, stackName, stackConfig, appEntries.map((e) => e.name));
3311
3462
  const project = this.registry.get(projectName);
3312
- const appDefinitions = await this.loadAppDefinitions(projectName, stackConfig, ecosystemConfig, appEntries);
3463
+ const appDefinitions = preservation ? this.preservationDefinitions(projectName, stackName, appEntries)
3464
+ : await this.loadAppDefinitions(projectName, stackConfig, ecosystemConfig, appEntries);
3313
3465
  // 2. Provision per-stack infrastructure (built-in + app-declared services)
3314
3466
  // Auto-detect core services (postgres, redis, minio) from app requirements,
3315
3467
  // then merge with explicit infrastructure config (gateway, custom services).
@@ -3319,8 +3471,13 @@ export class ProjectService extends EventEmitter {
3319
3471
  throw new Error(`Bootstrap declarations unavailable for ${entry.name}`);
3320
3472
  }
3321
3473
  const effectiveInfra = ownsInfrastructure ? mergeInferredInfrastructure(buildInfraFromRequirements(requirementsFromDefinitions(appDefinitions)), verifyArtifactInfrastructure ? await verifyArtifactInfrastructure() : mergeInfrastructure(ecosystemConfig, stackConfig)) : undefined;
3474
+ if (preservation) {
3475
+ const current = await this.reviewStackStart(projectName, stackName, { infrastructure: 'preserve', ...preservation.selection });
3476
+ if (!current.digest || current.digest !== preservation.digest)
3477
+ throw Error('Launch, release or native infrastructure changed immediately before start');
3478
+ }
3322
3479
  // Refuse missing/tampered migrators before creating or changing infrastructure.
3323
- if (effectiveInfra && project) {
3480
+ if (effectiveInfra && project && !preservation) {
3324
3481
  const migrations = await localMigrationTargets(requestedMigrations(normalizeInfraConfig(effectiveInfra, createDefaultRegistry())), appEntries, project.path, verifyMigrations);
3325
3482
  const resolveMigration = this.stackEntryBuilder(projectName, stackName, stackConfig, ecosystemConfig, appDefinitions, stackEnv);
3326
3483
  validateMigrationDestinations(migrations.map(target => {
@@ -3330,7 +3487,7 @@ export class ProjectService extends EventEmitter {
3330
3487
  }
3331
3488
  if (effectiveInfra) {
3332
3489
  try {
3333
- const infraService = await this.infraManager.provisionStack(projectName, stackName, stackConfig, effectiveInfra, appDefinitions, project?.path);
3490
+ const infraService = preservation ? await this.infraManager.preserveStack(projectName, stackName, stackConfig, effectiveInfra, preservation.targets.find(target => target.node === 'local').infrastructure.digest, appDefinitions, project?.path) : await this.infraManager.provisionStack(projectName, stackName, stackConfig, effectiveInfra, appDefinitions, project?.path, reconcile);
3334
3491
  state.infraService = infraService;
3335
3492
  const outcome = summariseProvisioning(infraService.getDesiredServices(), infraService.getState().services);
3336
3493
  if (!outcome.ready && !outcome.empty) {
@@ -3354,7 +3511,7 @@ export class ProjectService extends EventEmitter {
3354
3511
  // `settings.allowDegradedInfra: true` (the field is namespaced under
3355
3512
  // settings to remain forward-compatible with stack schema).
3356
3513
  const allowDegraded = stackConfig.settings?.['allowDegradedInfra'] === true;
3357
- if (!allowDegraded) {
3514
+ if (!allowDegraded || preservation) {
3358
3515
  const cleanMsg = `Stack '${projectName}/${stackName}' infrastructure provisioning failed:\n` +
3359
3516
  ` ${reason}\n\n` +
3360
3517
  `Apps were not started. Fix the underlying infrastructure issue (Docker socket, ` +
@@ -3373,7 +3530,7 @@ export class ProjectService extends EventEmitter {
3373
3530
  // Resolve once for both migrations and application startup. Inferred service
3374
3531
  // requirements are not permission to connect to a database or run migrations.
3375
3532
  const appConfigBuilder = this.stackEntryBuilder(projectName, stackName, stackConfig, ecosystemConfig, appDefinitions, stackEnv);
3376
- if (effectiveInfra && project) {
3533
+ if (effectiveInfra && project && !preservation) {
3377
3534
  await this.runStackMigrations(projectName, stackName, this.infraManager.getNormalizedServices(projectName, stackName) ?? {}, appEntries, project.path, appConfigBuilder, verifyMigrations);
3378
3535
  }
3379
3536
  await enable?.(appConfigBuilder);
@@ -3435,10 +3592,10 @@ export class ProjectService extends EventEmitter {
3435
3592
  * and silent about which of its seven steps was running. See
3436
3593
  * `deploy-phases.ts`.
3437
3594
  */
3438
- async startRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, release = null, reinstall = false) {
3595
+ async startRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, release = null, reinstall = false, preservation, reconcile = false) {
3439
3596
  const phases = reportPhases(this.logger, { project: projectName, stack: stackName });
3440
3597
  try {
3441
- return await withDeploymentProgress({ project: projectName, stack: stackName, operationId: randomUUID(), ...(release && { releaseId: release.id }) }, () => this.deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release, reinstall));
3598
+ return await withDeploymentProgress({ project: projectName, stack: stackName, operationId: randomUUID(), ...(release && { releaseId: release.id }) }, () => this.deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release, reinstall, preservation, reconcile));
3442
3599
  }
3443
3600
  finally {
3444
3601
  // Every exit, including the refusals: a reporter that outlives its
@@ -3446,7 +3603,7 @@ export class ProjectService extends EventEmitter {
3446
3603
  phases.done();
3447
3604
  }
3448
3605
  }
3449
- async deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release = null, reinstall = false) {
3606
+ async deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release = null, reinstall = false, preservation, reconcile = false) {
3450
3607
  const nodes = stackConfig.nodes ?? [];
3451
3608
  if (nodes.length === 0) {
3452
3609
  throw new Error(`Remote stack '${stackName}' has no nodes configured`);
@@ -3482,8 +3639,9 @@ export class ProjectService extends EventEmitter {
3482
3639
  const appEntries = this.resolveStackApps(stackConfig, ecosystemConfig);
3483
3640
  const migrationRequests = requestedMigrations({ ...declaredServices,
3484
3641
  ...normalizeInfraConfig(mergeInfrastructure(ecosystemConfig, stackConfig) ?? {}, createDefaultRegistry()) });
3485
- validateMigrationOwners(migrationRequests, appEntries);
3486
- const placements = orderMigrationPlacements(migrationRequests, resolveNodePlacements(stackConfig, appEntries));
3642
+ if (!preservation)
3643
+ validateMigrationOwners(migrationRequests, appEntries);
3644
+ const placements = orderMigrationPlacements(preservation ? [] : migrationRequests, resolveNodePlacements(stackConfig, appEntries));
3487
3645
  // What the stack gives its apps, vault references resolved — before any
3488
3646
  // node is touched: a key the vault does not hold refuses the deployment
3489
3647
  // here, not after a node was provisioned (project/stack-env.ts). A remote
@@ -3611,6 +3769,11 @@ export class ProjectService extends EventEmitter {
3611
3769
  machine: `${target.host}:${target.sshPort ?? 22}`,
3612
3770
  run: this.deployer.leaseRunner(target),
3613
3771
  })), `${projectName}/${stackName}`, this.logger, async (leases) => {
3772
+ if (preservation) {
3773
+ const current = await this.reviewStackStart(projectName, stackName, { infrastructure: 'preserve', ...preservation.selection });
3774
+ if (!current.digest || current.digest !== preservation.digest)
3775
+ throw Error('Preserved stack changed while acquiring deployment authority');
3776
+ }
3614
3777
  if (release && leases.unreachable.size) {
3615
3778
  const missing = [...leases.unreachable].map(([node, reason]) => `${node}: ${reason}`).join(', ');
3616
3779
  throw Error('Release deployment requires every selected node to be reachable before activation: ' + missing);
@@ -3651,11 +3814,11 @@ export class ProjectService extends EventEmitter {
3651
3814
  this.logger.warn({ node: nodeKey, because: master.reason }, 'Provisioning this node without a master address — it will be pulled from, not dial in');
3652
3815
  }
3653
3816
  await leases.confirm(nodeKey, `provisioning ${node.host}`);
3654
- const lock = await this.holdForDeployment(target, projectName, stackName, stackConfig, ecosystemConfig, release, phases);
3817
+ const lock = preservation ? null : await this.holdForDeployment(target, projectName, stackName, stackConfig, ecosystemConfig, release, phases);
3655
3818
  if (lock)
3656
3819
  locks.set(nodeKey, lock);
3657
3820
  phases.enter(`provisioning ${node.host}`);
3658
- const provisioned = await this.deployer.provisionSlaveNode(target, master.host, masterPort, projectName);
3821
+ const provisioned = preservation ? true : await this.deployer.provisionSlaveNode(target, master.host, masterPort, projectName);
3659
3822
  if (!provisioned) {
3660
3823
  skipped.push(nodeKey);
3661
3824
  this.logger.error({ node: nodeKey }, 'Failed to provision slave — skipping');
@@ -3682,7 +3845,7 @@ export class ProjectService extends EventEmitter {
3682
3845
  // Sending only the stack's own block is why a remote deployment had
3683
3846
  // databases and no gateway, and no onion address to reach it by.
3684
3847
  const nodeInfra = mergeInfrastructure(ecosystemConfig, stackConfig);
3685
- if (nodeInfra || Object.keys(declaredServices).length > 0) {
3848
+ if (preservation || nodeInfra || Object.keys(declaredServices).length > 0) {
3686
3849
  await leases.confirm(nodeKey, `bringing up infrastructure on ${node.host}`);
3687
3850
  phases.enter(`bringing up infrastructure on ${node.host}`);
3688
3851
  const infrastructure = await this.provisionNodeInfrastructure(connector, node, (nodeInfra ?? {}), declaredServices,
@@ -3694,10 +3857,11 @@ export class ProjectService extends EventEmitter {
3694
3857
  project: projectName,
3695
3858
  stack: stackName,
3696
3859
  overrides: await this.resolveOverrideSecrets(projectName, stackConfig.serviceOverrides),
3697
- }, project?.path, release?.staticsDir ?? undefined);
3860
+ }, project?.path, release?.staticsDir ?? undefined, preservation ? { mode: 'preserve', review: preservation.targets.find(preservedTarget => preservedTarget.node === nodeKey)?.infrastructure?.digest ?? undefined }
3861
+ : reconcile ? { mode: 'reconcile' } : undefined);
3698
3862
  if (!infrastructure.ready) {
3699
- if (release) {
3700
- throw new Error(`Release ${release.id} stopped before application installation on ${nodeKey}: ${infrastructure.detail}`);
3863
+ if (release || preservation) {
3864
+ throw new Error(`Deployment stopped before application installation on ${nodeKey}: ${infrastructure.detail}`);
3701
3865
  }
3702
3866
  // Not fatal: a node whose infrastructure is incomplete can still
3703
3867
  // be looked at, and stopping here would leave the fleet in a state
@@ -3763,7 +3927,7 @@ export class ProjectService extends EventEmitter {
3763
3927
  const results = await this.deployer.deployToStack([target], nodeArtifacts, projectName, {
3764
3928
  apps: nodeApps,
3765
3929
  appEnv,
3766
- migrations: migrationRequests.filter(request => selectedNames.has(request.app)),
3930
+ migrations: preservation ? [] : migrationRequests.filter(request => selectedNames.has(request.app)),
3767
3931
  stack: stackName,
3768
3932
  // Past the node's record of what each app runs: ship and restart all.
3769
3933
  ...(reinstall ? { force: true } : {}),
@@ -4183,7 +4347,7 @@ export class ProjectService extends EventEmitter {
4183
4347
  /** Filled with each service whose directory was sent → that directory's absolute path here. */
4184
4348
  shippedDirs,
4185
4349
  /** The services the stack's apps require — each may name a directory of its own, as the stack's may. */
4186
- services) {
4350
+ services, strict = false) {
4187
4351
  const { readConfigDirectory } = await import('../infrastructure/config-payload.js');
4188
4352
  const { shippedDirsOf } = await import('../infrastructure/shipped-config.js');
4189
4353
  const fsp = await import('node:fs/promises');
@@ -4206,6 +4370,8 @@ export class ProjectService extends EventEmitter {
4206
4370
  shippedDirs?.set(name, abs);
4207
4371
  }
4208
4372
  if (skipped.length > 0) {
4373
+ if (strict)
4374
+ throw Error('Complete service configuration is required for preservation');
4209
4375
  // Said out loud: a file left behind changes what the service does,
4210
4376
  // and silence here would make the node's behaviour unexplainable
4211
4377
  // from the master.
@@ -4213,6 +4379,8 @@ export class ProjectService extends EventEmitter {
4213
4379
  }
4214
4380
  }
4215
4381
  catch (err) {
4382
+ if (strict)
4383
+ throw Error('Service configuration cannot be verified for preservation', { cause: err });
4216
4384
  // Not fatal. A stack whose gateway config is missing still gets its
4217
4385
  // databases, and the gateway fails visibly rather than the whole
4218
4386
  // provisioning stopping before anything was done.
@@ -4221,6 +4389,12 @@ export class ProjectService extends EventEmitter {
4221
4389
  }
4222
4390
  return payload;
4223
4391
  }
4392
+ async reviewNodeInfrastructure(node, infrastructure, services, owner, projectRoot, releasedStatics) {
4393
+ const result = await this.provisionNodeInfrastructure(this.getSlaveConnector(), node, infrastructure, services, owner, projectRoot, releasedStatics, { mode: 'review' });
4394
+ if (!result.preservation || result.preservation.protocol !== 1 || result.preservation.mode !== 'preserve')
4395
+ throw Error('Recipient does not confirm infrastructure preservation capability; update or inspect the node before recovery');
4396
+ return result.preservation;
4397
+ }
4224
4398
  async provisionNodeInfrastructure(connector, node, infrastructure, services, owner,
4225
4399
  /**
4226
4400
  * This project's root on the MASTER, for reading the config files a
@@ -4235,7 +4409,7 @@ export class ProjectService extends EventEmitter {
4235
4409
  */
4236
4410
  projectRoot,
4237
4411
  /** A release's static bundle, shipped as it was built — see `shipStackStatics`. */
4238
- releasedStatics) {
4412
+ releasedStatics, authority) {
4239
4413
  const host = node.host;
4240
4414
  const port = node.port ?? 9700;
4241
4415
  await connector.addSlave({ host, port, label: node.label });
@@ -4248,7 +4422,8 @@ export class ProjectService extends EventEmitter {
4248
4422
  }
4249
4423
  try {
4250
4424
  const shippedDirs = new Map();
4251
- const configFiles = projectRoot ? await this.readStackConfigFiles(infrastructure, projectRoot, shippedDirs, services) : {};
4425
+ const preserving = authority?.mode === 'review' || authority?.mode === 'preserve';
4426
+ const configFiles = projectRoot ? await this.readStackConfigFiles(infrastructure, projectRoot, shippedDirs, services, preserving) : {};
4252
4427
  // A service's bind mounts inside its shipped directory name the node's
4253
4428
  // copy, not this machine's path — the stack's own services and the ones
4254
4429
  // its apps require alike. See `shipped-config.ts`.
@@ -4261,16 +4436,31 @@ export class ProjectService extends EventEmitter {
4261
4436
  // by SSH rather than inside the call: 32 MB is the wrong size for an
4262
4437
  // RPC argument, which is held whole on both sides and blocks the call
4263
4438
  // it rides on. Config files are 65 KB and ride along; a build does not.
4264
- const shipped = projectRoot
4439
+ const shipped = projectRoot && !preserving
4265
4440
  ? await this.shipStackStatics(infrastructure, projectRoot, node, releasedStatics, owner)
4266
4441
  : { dirs: {}, roots: {} };
4267
4442
  const staticRoots = shipped.dirs;
4268
4443
  const staticLinkRoots = shipped.roots;
4444
+ const staticDigests = {};
4445
+ if (preserving && projectRoot && this.staticDirOf(infrastructure, projectRoot)) {
4446
+ const { releaseTreeDigest } = await import('../release/runtime-provenance.js');
4447
+ staticDigests['gateway'] = await releaseTreeDigest(releasedStatics ?? this.staticDirOf(infrastructure, projectRoot), true);
4448
+ }
4269
4449
  // Retry once if the connection turns out to be gone: the deployer just
4270
4450
  // restarted this node's daemon, so the mesh connection the master holds
4271
4451
  // was established to the process that exited. `provisionStack` is a
4272
4452
  // reconciler, so applying it twice is applying it once.
4273
- const report = (await connector.invokeOnSlave(host, port, 'OmnitronInfra', 'provisionStack', [{ config: configForNode, services: servicesForNode, ...(owner ?? {}), configFiles, staticRoots, staticLinkRoots }], { retryOnDisconnect: true }));
4453
+ const report = (await connector.invokeOnSlave(host, port, 'OmnitronInfra', authority?.mode === 'review' ? 'reviewStack' : 'provisionStack', [{ config: configForNode, services: servicesForNode, ...(owner ?? {}), configFiles, staticRoots, staticLinkRoots,
4454
+ ...(preserving ? { infrastructure: 'preserve', staticDigests, ...(authority?.review && { review: authority.review }) }
4455
+ : authority?.mode === 'reconcile' ? { infrastructure: 'reconcile' } : {}) }], { retryOnDisconnect: true }));
4456
+ const preservation = authority?.mode === 'review' ? report : report?.preservation;
4457
+ if (preserving) {
4458
+ if (preservation?.protocol !== 1 || preservation.mode !== 'preserve' || !Array.isArray(preservation.checks) || !Array.isArray(preservation.containers))
4459
+ throw Error('Recipient does not implement the preservation protocol');
4460
+ if (authority?.mode === 'preserve' && (!preservation.digest || preservation.digest !== authority.review))
4461
+ throw Error('Recipient did not confirm the reviewed infrastructure identity');
4462
+ return { ready: preservation.digest !== null, detail: 'Native infrastructure preservation checked', preservation };
4463
+ }
4274
4464
  this.logger.info({
4275
4465
  node: `${host}:${port}`,
4276
4466
  ready: report?.ready ?? false,
@@ -4287,6 +4477,8 @@ export class ProjectService extends EventEmitter {
4287
4477
  // touched, and going on would put new backends behind the old frontend.
4288
4478
  if (err instanceof FrontendNotDeliveredError)
4289
4479
  throw err;
4480
+ if (authority?.mode === 'review' || authority?.mode === 'preserve')
4481
+ return { ready: false, detail: 'Recipient preservation capability or complete native identity could not be verified; no provisioning fallback is permitted' };
4290
4482
  this.logger.error({ node: `${host}:${port}`, error: err.message }, 'Could not bring up this node\'s infrastructure');
4291
4483
  return { ready: false, detail: `could not ask the node: ${err.message}` };
4292
4484
  }
@@ -4450,7 +4642,7 @@ export class ProjectService extends EventEmitter {
4450
4642
  if (entry.bootstrap && project) {
4451
4643
  try {
4452
4644
  const bootstrapAbsPath = resolveAppPath(project.path, entry, entry.bootstrap);
4453
- const { definition: loaded, metadata } = await readBootstrapMetadata(bootstrapAbsPath);
4645
+ const { definition: loaded, metadata } = await readBootstrapMetadata(bootstrapAbsPath, false, 30_000, entry.releaseRoot);
4454
4646
  this.bootstrapMetadata.save(metadata);
4455
4647
  // Preserve only declarative data across backend handoffs, never executable hooks.
4456
4648
  this.bootstrapTopology.set(bootstrapAbsPath, structuredClone({ ...(loaded.requires ? { requires: loaded.requires } : {}), ...(loaded.omnitronConfig ? { omnitronConfig: loaded.omnitronConfig } : {}) }));
@@ -4517,37 +4709,34 @@ export class ProjectService extends EventEmitter {
4517
4709
  throw new Error('Project is not registered');
4518
4710
  return new LocalReleaseSelection(this.daemonStateStore, { project, stack }, registration.registeredAt);
4519
4711
  }
4520
- async installedEntries(project, stack, definition, config, release, prepare = false, requiredMigrations, migrationFiles) {
4521
- return (await this.installedRelease(project, stack, definition, config, release, prepare, requiredMigrations, migrationFiles)).entries;
4712
+ async installedEntries(project, stack, definition, config, release, prepare = false, requiredApps, requiredFiles) {
4713
+ return (await this.installedRelease(project, stack, definition, config, release, prepare, requiredApps, requiredFiles)).entries;
4522
4714
  }
4523
- async installedRelease(project, stack, definition, config, release, prepare = false, requiredMigrations, migrationFiles) {
4715
+ async installedRelease(project, stack, definition, config, release, prepare = false, requiredApps, requiredFiles, admittedOwner) {
4524
4716
  const root = this.registry.get(project)?.path;
4525
4717
  if (!root)
4526
4718
  throw new Error('Project is not registered');
4527
4719
  const { inspectLocalReleaseInstallation, inspectLocalReleasePayloads } = await import('../release/local-installation.js');
4528
4720
  const stackEntries = this.resolveStackApps(definition, config);
4529
4721
  // Validate the complete stack before narrowing executable verification to
4530
- // migration owners. Other applications still own their declared environment.
4722
+ // requested executable owners. Other applications still own their declared environment.
4531
4723
  const env = await this.stackEnvFor(project, stack, definition, stackEntries.map(entry => entry.name));
4532
- const configured = stackEntries.filter(entry => !requiredMigrations || requiredMigrations.includes(entry.name));
4724
+ const configured = stackEntries.filter(entry => !requiredApps || requiredApps.includes(entry.name));
4533
4725
  const entries = configured.map(entry => ({
4534
4726
  ...entry, env: { ...entry.env, ...env[entry.name] },
4535
4727
  ...(!entry.releaseFrom || entry.cwd !== undefined ? { cwd: resolveAppPath(root, entry) } : {}),
4536
4728
  ...(entry.bootstrap && { bootstrap: resolveAppPath(root, entry, entry.bootstrap) }),
4537
4729
  }));
4538
- const ownerPid = this.orchestrator.getExecutionOwnerPid?.();
4539
- if (!ownerPid)
4540
- throw new Error('A verified local release requires a connected independent execution owner');
4541
- const ownerPackage = await this.orchestrator.getExecutionOwnerPackage?.();
4542
- if (!ownerPackage)
4543
- throw new Error('Verified runtime origin requires an independently verified execution-owner package');
4730
+ const owner = admittedOwner ?? await this.orchestrator.getExecutionOwnerRuntime?.();
4731
+ assertResidentApplicationRuntime(owner);
4732
+ assertReleaseTarget(owner.target, owner.target);
4544
4733
  const infrastructure = mergeInfrastructure(config, definition);
4545
4734
  const payloads = artifactVolumeEntries(infrastructure, config.apps);
4546
4735
  const roots = await inspectLocalReleasePayloads(release, payloads, prepare);
4547
4736
  const resolved = await bindArtifactVolumes(infrastructure, roots);
4548
- const installed = await inspectLocalReleaseInstallation(release, stack, entries, { prepare, ownerPid, ownerPackage,
4549
- ...(requiredMigrations && { requiredFiles: migrationFiles ?? Object.fromEntries(requiredMigrations.map(app => [app, [COMPILED_MIGRATION_ENTRY]])) }),
4550
- });
4737
+ const installed = await inspectLocalReleaseInstallation(release, stack, entries, { prepare,
4738
+ ownerPid: owner.execution.pid, ownerPackage: owner.root, runtime: owner.target,
4739
+ ...(requiredApps && { requiredFiles: requiredFiles ?? Object.fromEntries(requiredApps.map(app => [app, [COMPILED_MIGRATION_ENTRY]])) }), });
4551
4740
  const preparedArtifactVolumes = artifactVolumeBindings(infrastructure).map(([binding, value]) => {
4552
4741
  const service = resolved?.services?.[value.service];
4553
4742
  const mount = service && !('preset' in service) ? service.docker?.volumes?.[binding] : undefined;
@@ -4559,6 +4748,84 @@ export class ProjectService extends EventEmitter {
4559
4748
  });
4560
4749
  return { entries: installed, infrastructure: resolved, preparedArtifactVolumes };
4561
4750
  }
4751
+ /** Read-only admission for an operation in an already selected, prepared release.
4752
+ * The returned fence repeats admission before each Xec attempt; it cannot install,
4753
+ * select a release, run setup, or silently adopt a different owner/registration. */
4754
+ async resolvePipelineRelease(target, options) {
4755
+ const { readInstalledReleaseReceipt } = await import('../release/installed-origin.js');
4756
+ const { frozenMember } = await import('../release/frozen-installation.js');
4757
+ const inspect = async () => {
4758
+ assertReleaseLoaderEnvironment(process.env);
4759
+ assertReleaseLoaderEnvironment(options.env);
4760
+ const scope = await this.resolveProjectStack({ project: target.project, stack: target.stack }, { readOnly: true });
4761
+ this.assertStackMutable(scope);
4762
+ const registered = this.registry.get(scope.project);
4763
+ const config = this.getLoadedConfig(scope.project);
4764
+ if (!registered || !config)
4765
+ throw Error('Release pipeline target requires a registered, loaded project');
4766
+ const registration = structuredClone(registered);
4767
+ const definition = this.resolveStacks(config, scope.project)[scope.stack];
4768
+ if (!definition || definition.type !== 'local' || definition.managedOrigin)
4769
+ throw Error('Release pipeline target requires an independently owned local stack; remote execution is unsupported');
4770
+ const fingerprint = () => createHash('sha256').update(JSON.stringify({
4771
+ definition: this.resolveStacks(this.getLoadedConfig(scope.project), scope.project)[scope.stack],
4772
+ config: this.getLoadedConfig(scope.project),
4773
+ })).digest('hex');
4774
+ const definitionFingerprint = fingerprint();
4775
+ const selection = this.localSelection(scope.project, scope.stack).read();
4776
+ if (!selection.release)
4777
+ throw Error('Release pipeline target has no selected immutable release');
4778
+ const owner = await this.orchestrator.getExecutionOwnerRuntime?.();
4779
+ assertResidentApplicationRuntime(owner);
4780
+ assertReleaseTarget(owner.target, owner.target);
4781
+ if (!owner.execution?.lifetime)
4782
+ throw Error('Release pipeline execution requires native owner lifetime evidence');
4783
+ const release = await this.admitRelease(scope.project, scope.stack, definition, config, selection.release.id, 'check');
4784
+ if (release.manifestSha256 !== selection.release.manifestSha256)
4785
+ throw Error('Selected release manifest changed on disk');
4786
+ const source = releaseArtifactOrigins(release.manifest, release.manifestSha256).find(item => item.app === target.app);
4787
+ const origin = source?.origin;
4788
+ const artifact = origin?.runtimeOrigin && { releaseId: origin.releaseId, manifestSha256: origin.manifestSha256,
4789
+ artifactSha256: origin.artifactSha256, producerApp: origin.runtimeOrigin.producerApp,
4790
+ projectCommit: origin.runtimeOrigin.projectCommit, frameworkCommit: origin.frameworkCommit };
4791
+ if (!artifact || !isDeepStrictEqual(artifact, target.artifact))
4792
+ throw Error('Selected release does not contain the pinned pipeline artifact');
4793
+ const installed = await this.installedRelease(scope.project, scope.stack, definition, config, release, false, [target.app], { [target.app]: [target.entry] }, owner);
4794
+ if (installed.entries.length !== 1 || !installed.entries[0]?.releaseRoot)
4795
+ throw Error('Pipeline application is absent from the selected stack');
4796
+ const entry = installed.entries[0], root = entry.releaseRoot;
4797
+ const file = await frozenMember(root, target.entry);
4798
+ const receipt = await readInstalledReleaseReceipt(root);
4799
+ const env = { ...Object.fromEntries(Object.entries(process.env).filter((item) => typeof item[1] === 'string')),
4800
+ ...entry.env, ...options.env, NODE_OPTIONS: releaseOptionsForEnv(entry.env), NODE_PATH: '',
4801
+ PATH: path.dirname(owner.executable) + path.delimiter + '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' };
4802
+ const cwd = options.cwd ?? entry.cwd ?? root;
4803
+ if (!path.isAbsolute(cwd) || await fs.promises.realpath(cwd) !== cwd || !(await fs.promises.stat(cwd)).isDirectory())
4804
+ throw Error('A release pipeline working directory must be an absolute physical directory');
4805
+ const command = { command: owner.executable, args: [file, ...options.args ?? []], shell: false,
4806
+ cwd,
4807
+ env: withClosureLibraryPath(env, { verifiedRoot: root }, {}) ?? env };
4808
+ const evidence = { artifact,
4809
+ consumer: origin.runtimeOrigin.consumer,
4810
+ installationRoot: root, treeSha256: receipt.treeSha256, entry: file, executable: owner.executable, cwd,
4811
+ environmentSha256: createHash('sha256').update(JSON.stringify(Object.entries(command.env).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))).digest('hex'), target: owner.target,
4812
+ owner: { id: owner.execution.ownerId, pid: owner.execution.pid, epoch: owner.execution.epoch,
4813
+ lifetime: owner.execution.lifetime, frameworkCommit: owner.commit } };
4814
+ const observed = await this.orchestrator.getExecutionOwnerRuntime?.();
4815
+ assertSameApplicationRuntime(owner, observed);
4816
+ assertReleaseTarget(owner.target, observed?.target);
4817
+ this.assertStackMutable(scope);
4818
+ if (!isDeepStrictEqual(registration, this.registry.get(scope.project)) ||
4819
+ !isDeepStrictEqual(selection, this.localSelection(scope.project, scope.stack).read()) || fingerprint() !== definitionFingerprint)
4820
+ throw Error('Pipeline registration, configuration or release selection changed during admission');
4821
+ return { command, evidence, registration, selection, scope, definition: definitionFingerprint };
4822
+ };
4823
+ const admitted = await inspect();
4824
+ return { command: admitted.command, evidence: admitted.evidence, verify: async () => {
4825
+ if (!isDeepStrictEqual(admitted, await inspect()))
4826
+ throw Error('Pipeline release, application environment or execution owner changed after admission');
4827
+ } };
4828
+ }
4562
4829
  /** All consumers of executable declarations must see the same physical entry as the owner. */
4563
4830
  async launchEntries(project, stack, definition, config, requiredMigrations, migrationFiles) {
4564
4831
  const selected = definition.type === 'local' && !definition.managedOrigin ? this.localSelection(project, stack).read().release : null;
@@ -4927,6 +5194,7 @@ export class ProjectService extends EventEmitter {
4927
5194
  const selectedRelease = config.type === 'local' && !config.managedOrigin ? this.localSelection(projectName, stackName).read().release : null;
4928
5195
  return {
4929
5196
  name: stackName,
5197
+ infrastructureMode: this.infrastructurePolicy(projectName, stackName)?.mode ?? 'reconcile',
4930
5198
  type: config.type,
4931
5199
  ...(selectedRelease && { selectedRelease }),
4932
5200
  status,