@hivemagnus/magnus-server 0.0.0-stage → 1.0.2

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 (356) hide show
  1. package/.env.example +171 -0
  2. package/LICENSE +202 -0
  3. package/README.md +169 -2
  4. package/agent-version.json +1 -0
  5. package/dist/ai/HEARTBEAT.md +56 -0
  6. package/dist/ai/IDENTITY.md +54 -0
  7. package/dist/ai/MEMORY.md +40 -0
  8. package/dist/ai/TOOLS.md +538 -0
  9. package/dist/ai/USER.md +32 -0
  10. package/dist/cli/args.js +62 -0
  11. package/dist/cli/env-catalog.js +131 -0
  12. package/dist/cli/preflight.js +89 -0
  13. package/dist/cli.js +175 -0
  14. package/dist/config/cors.js +16 -0
  15. package/dist/config/env.js +392 -0
  16. package/dist/config/features.js +16 -0
  17. package/dist/config/index.js +3 -0
  18. package/dist/config/listener-security.js +38 -0
  19. package/dist/config/version.js +31 -0
  20. package/dist/db/client.js +44 -0
  21. package/dist/db/generated/prisma/browser.js +18 -0
  22. package/dist/db/generated/prisma/client.js +35 -0
  23. package/dist/db/generated/prisma/commonInputTypes.js +11 -0
  24. package/dist/db/generated/prisma/enums.js +42 -0
  25. package/dist/db/generated/prisma/internal/class.js +50 -0
  26. package/dist/db/generated/prisma/internal/prismaNamespace.js +1598 -0
  27. package/dist/db/generated/prisma/internal/prismaNamespaceBrowser.js +1569 -0
  28. package/dist/db/generated/prisma/models/A2ATask.js +2 -0
  29. package/dist/db/generated/prisma/models/AdmissionFence.js +2 -0
  30. package/dist/db/generated/prisma/models/Agent.js +2 -0
  31. package/dist/db/generated/prisma/models/AiConversation.js +2 -0
  32. package/dist/db/generated/prisma/models/AiDashboard.js +2 -0
  33. package/dist/db/generated/prisma/models/AiMemory.js +2 -0
  34. package/dist/db/generated/prisma/models/AiMessage.js +2 -0
  35. package/dist/db/generated/prisma/models/AiScheduledOp.js +2 -0
  36. package/dist/db/generated/prisma/models/AlertConditionState.js +2 -0
  37. package/dist/db/generated/prisma/models/AlertRule.js +2 -0
  38. package/dist/db/generated/prisma/models/AlertViolation.js +2 -0
  39. package/dist/db/generated/prisma/models/AnomalyEvent.js +2 -0
  40. package/dist/db/generated/prisma/models/ApplicationObservation.js +2 -0
  41. package/dist/db/generated/prisma/models/ArdentInstallation.js +2 -0
  42. package/dist/db/generated/prisma/models/ArdentProfile.js +2 -0
  43. package/dist/db/generated/prisma/models/ArdentScopeEnrollment.js +2 -0
  44. package/dist/db/generated/prisma/models/CommandResourceFence.js +2 -0
  45. package/dist/db/generated/prisma/models/CommandResourceLease.js +2 -0
  46. package/dist/db/generated/prisma/models/CommandResult.js +2 -0
  47. package/dist/db/generated/prisma/models/CronJob.js +2 -0
  48. package/dist/db/generated/prisma/models/CronJobExecution.js +2 -0
  49. package/dist/db/generated/prisma/models/DeployJob.js +2 -0
  50. package/dist/db/generated/prisma/models/DriftScan.js +2 -0
  51. package/dist/db/generated/prisma/models/EngineeringMetricSample.js +2 -0
  52. package/dist/db/generated/prisma/models/EngineeringRecoveryExercise.js +2 -0
  53. package/dist/db/generated/prisma/models/ExecutionRun.js +2 -0
  54. package/dist/db/generated/prisma/models/FilePatchTask.js +2 -0
  55. package/dist/db/generated/prisma/models/GpuSnapshot.js +2 -0
  56. package/dist/db/generated/prisma/models/HealthSnapshot.js +2 -0
  57. package/dist/db/generated/prisma/models/HermesCoreRollout.js +2 -0
  58. package/dist/db/generated/prisma/models/HermesCoreRolloutHost.js +2 -0
  59. package/dist/db/generated/prisma/models/HermesCredentialPool.js +2 -0
  60. package/dist/db/generated/prisma/models/HermesCredentialPoolAttachment.js +2 -0
  61. package/dist/db/generated/prisma/models/HermesCronJob.js +2 -0
  62. package/dist/db/generated/prisma/models/HermesHook.js +2 -0
  63. package/dist/db/generated/prisma/models/HermesInstallation.js +2 -0
  64. package/dist/db/generated/prisma/models/HermesMcpServer.js +2 -0
  65. package/dist/db/generated/prisma/models/HermesMcpServerAttachment.js +2 -0
  66. package/dist/db/generated/prisma/models/HermesMessagingBinding.js +2 -0
  67. package/dist/db/generated/prisma/models/HermesOperationJob.js +2 -0
  68. package/dist/db/generated/prisma/models/HermesPluginInstall.js +2 -0
  69. package/dist/db/generated/prisma/models/HermesProfile.js +2 -0
  70. package/dist/db/generated/prisma/models/HermesProviderConfig.js +2 -0
  71. package/dist/db/generated/prisma/models/HermesRegistryClient.js +2 -0
  72. package/dist/db/generated/prisma/models/HermesRegistryInventory.js +2 -0
  73. package/dist/db/generated/prisma/models/HermesRegistryReceipt.js +2 -0
  74. package/dist/db/generated/prisma/models/HermesRegistryRevision.js +2 -0
  75. package/dist/db/generated/prisma/models/HermesRegistryTarget.js +2 -0
  76. package/dist/db/generated/prisma/models/HermesRole.js +2 -0
  77. package/dist/db/generated/prisma/models/HermesRoleApplication.js +2 -0
  78. package/dist/db/generated/prisma/models/HermesSkillAssignment.js +2 -0
  79. package/dist/db/generated/prisma/models/HermesToolsetPolicy.js +2 -0
  80. package/dist/db/generated/prisma/models/IncidentLesson.js +2 -0
  81. package/dist/db/generated/prisma/models/LibraryCategory.js +2 -0
  82. package/dist/db/generated/prisma/models/LibraryFile.js +2 -0
  83. package/dist/db/generated/prisma/models/LogEntry.js +2 -0
  84. package/dist/db/generated/prisma/models/MagnusReleaseCanary.js +2 -0
  85. package/dist/db/generated/prisma/models/MaintenanceWindow.js +2 -0
  86. package/dist/db/generated/prisma/models/ManagedCronDefinition.js +2 -0
  87. package/dist/db/generated/prisma/models/ManagedCronOccurrence.js +2 -0
  88. package/dist/db/generated/prisma/models/Node.js +2 -0
  89. package/dist/db/generated/prisma/models/Notification.js +2 -0
  90. package/dist/db/generated/prisma/models/NotificationPreference.js +2 -0
  91. package/dist/db/generated/prisma/models/RemediationExecution.js +2 -0
  92. package/dist/db/generated/prisma/models/RemediationRule.js +2 -0
  93. package/dist/db/generated/prisma/models/RemoteCommand.js +2 -0
  94. package/dist/db/generated/prisma/models/ResourceDrainParticipant.js +2 -0
  95. package/dist/db/generated/prisma/models/ResourceEnrollment.js +2 -0
  96. package/dist/db/generated/prisma/models/ResourceParticipantWork.js +2 -0
  97. package/dist/db/generated/prisma/models/Role.js +2 -0
  98. package/dist/db/generated/prisma/models/RoleApplication.js +2 -0
  99. package/dist/db/generated/prisma/models/RolloutTarget.js +2 -0
  100. package/dist/db/generated/prisma/models/SecurityExportOutbox.js +2 -0
  101. package/dist/db/generated/prisma/models/SecurityFusionAction.js +2 -0
  102. package/dist/db/generated/prisma/models/SecurityFusionActionApproval.js +2 -0
  103. package/dist/db/generated/prisma/models/SecurityFusionCase.js +2 -0
  104. package/dist/db/generated/prisma/models/SecurityFusionCaseActivity.js +2 -0
  105. package/dist/db/generated/prisma/models/SecurityFusionCaseEntity.js +2 -0
  106. package/dist/db/generated/prisma/models/SecurityFusionCaseEvent.js +2 -0
  107. package/dist/db/generated/prisma/models/SecurityFusionCaseRelationship.js +2 -0
  108. package/dist/db/generated/prisma/models/SecurityFusionCollector.js +2 -0
  109. package/dist/db/generated/prisma/models/SecurityFusionCommandOutbox.js +2 -0
  110. package/dist/db/generated/prisma/models/SecurityFusionDeletionReceipt.js +2 -0
  111. package/dist/db/generated/prisma/models/SecurityFusionDelivery.js +2 -0
  112. package/dist/db/generated/prisma/models/SecurityFusionEntity.js +2 -0
  113. package/dist/db/generated/prisma/models/SecurityFusionEntityAlias.js +2 -0
  114. package/dist/db/generated/prisma/models/SecurityFusionEvent.js +2 -0
  115. package/dist/db/generated/prisma/models/SecurityFusionEventEntity.js +2 -0
  116. package/dist/db/generated/prisma/models/SecurityFusionEvidence.js +2 -0
  117. package/dist/db/generated/prisma/models/SecurityFusionInvestigation.js +2 -0
  118. package/dist/db/generated/prisma/models/SecurityFusionNonce.js +2 -0
  119. package/dist/db/generated/prisma/models/SecurityFusionReceiptChainHead.js +2 -0
  120. package/dist/db/generated/prisma/models/SecurityFusionRuleVersion.js +2 -0
  121. package/dist/db/generated/prisma/models/SecurityFusionSourceState.js +2 -0
  122. package/dist/db/generated/prisma/models/SecurityServiceState.js +2 -0
  123. package/dist/db/generated/prisma/models/SharedResourceFence.js +2 -0
  124. package/dist/db/generated/prisma/models/SharedResourceLease.js +2 -0
  125. package/dist/db/generated/prisma/models/SharedResourceLeaseResource.js +2 -0
  126. package/dist/db/generated/prisma/models/SkillInstallation.js +2 -0
  127. package/dist/db/generated/prisma/models/SkillRegistryEntry.js +2 -0
  128. package/dist/db/generated/prisma/models/SkillTag.js +2 -0
  129. package/dist/db/generated/prisma/models/SkillVersion.js +2 -0
  130. package/dist/db/generated/prisma/models/TeamApprovalDecision.js +2 -0
  131. package/dist/db/generated/prisma/models/User.js +2 -0
  132. package/dist/db/generated/prisma/models/WebhookDelivery.js +2 -0
  133. package/dist/db/generated/prisma/models/WebhookEndpoint.js +2 -0
  134. package/dist/db/generated/prisma/models.js +2 -0
  135. package/dist/db/index.js +3 -0
  136. package/dist/db/read-retry.js +35 -0
  137. package/dist/db/seed.js +348 -0
  138. package/dist/index.js +162 -0
  139. package/dist/middleware/auth.js +361 -0
  140. package/dist/middleware/body-limit.js +14 -0
  141. package/dist/middleware/correlation-id.js +13 -0
  142. package/dist/middleware/error-handler.js +28 -0
  143. package/dist/middleware/iam-token.js +21 -0
  144. package/dist/middleware/rate-limit.js +78 -0
  145. package/dist/middleware/request-logger.js +85 -0
  146. package/dist/middleware/security-headers.js +23 -0
  147. package/dist/middleware/tracing.js +120 -0
  148. package/dist/middleware/user-status.js +33 -0
  149. package/dist/openapi/spec.js +2656 -0
  150. package/dist/queue/redis.js +20 -0
  151. package/dist/queue/requeue.js +27 -0
  152. package/dist/queue/workers.js +342 -0
  153. package/dist/routes/ai-schedule.js +73 -0
  154. package/dist/routes/alerts.js +79 -0
  155. package/dist/routes/anomalies.js +101 -0
  156. package/dist/routes/ardent.js +82 -0
  157. package/dist/routes/assistant.js +417 -0
  158. package/dist/routes/audit.js +138 -0
  159. package/dist/routes/codex.js +65 -0
  160. package/dist/routes/commands.js +226 -0
  161. package/dist/routes/credentials.js +58 -0
  162. package/dist/routes/cron.js +292 -0
  163. package/dist/routes/dependencies.js +49 -0
  164. package/dist/routes/deploy.js +18 -0
  165. package/dist/routes/discovery.js +168 -0
  166. package/dist/routes/docs.js +24 -0
  167. package/dist/routes/drift.js +126 -0
  168. package/dist/routes/events-stream.js +87 -0
  169. package/dist/routes/files.js +214 -0
  170. package/dist/routes/fleet-checks.js +21 -0
  171. package/dist/routes/health.js +133 -0
  172. package/dist/routes/heartbeat.js +545 -0
  173. package/dist/routes/hermes-registry.js +278 -0
  174. package/dist/routes/hermes.js +61 -0
  175. package/dist/routes/library.js +402 -0
  176. package/dist/routes/logs.js +172 -0
  177. package/dist/routes/managed-cron.js +23 -0
  178. package/dist/routes/mcp.js +39 -0
  179. package/dist/routes/metrics.js +187 -0
  180. package/dist/routes/nodes.js +951 -0
  181. package/dist/routes/notifications.js +130 -0
  182. package/dist/routes/ops.js +258 -0
  183. package/dist/routes/orchestra.js +430 -0
  184. package/dist/routes/predictions.js +23 -0
  185. package/dist/routes/profiling.js +68 -0
  186. package/dist/routes/quick-command.js +72 -0
  187. package/dist/routes/remediation.js +111 -0
  188. package/dist/routes/resource-leases.js +297 -0
  189. package/dist/routes/roles.js +370 -0
  190. package/dist/routes/rollout.js +124 -0
  191. package/dist/routes/scan.js +116 -0
  192. package/dist/routes/security-backup.js +58 -0
  193. package/dist/routes/security-fusion.js +605 -0
  194. package/dist/routes/security-integration.js +71 -0
  195. package/dist/routes/self-update.js +129 -0
  196. package/dist/routes/shared-resource-leases.js +78 -0
  197. package/dist/routes/skills.js +485 -0
  198. package/dist/routes/stats.js +96 -0
  199. package/dist/routes/team-approvals.js +23 -0
  200. package/dist/routes/terminal-ticket.js +15 -0
  201. package/dist/routes/terminal.js +17 -0
  202. package/dist/routes/topology.js +37 -0
  203. package/dist/routes/users.js +219 -0
  204. package/dist/routes/webhooks.js +137 -0
  205. package/dist/schemas/command-reconciliation.js +14 -0
  206. package/dist/schemas/commands.js +51 -0
  207. package/dist/schemas/cron.js +33 -0
  208. package/dist/schemas/deploy.js +20 -0
  209. package/dist/schemas/files.js +14 -0
  210. package/dist/schemas/hermes.js +69 -0
  211. package/dist/schemas/index.js +11 -0
  212. package/dist/schemas/library.js +48 -0
  213. package/dist/schemas/logs.js +35 -0
  214. package/dist/schemas/managed-cron.js +127 -0
  215. package/dist/schemas/node-responses.js +87 -0
  216. package/dist/schemas/node-session-receipt.js +84 -0
  217. package/dist/schemas/nodes.js +36 -0
  218. package/dist/schemas/orchestra.js +118 -0
  219. package/dist/schemas/resource-participant-reconciliation.js +99 -0
  220. package/dist/schemas/roles.js +54 -0
  221. package/dist/schemas/shared-resource-lease.js +132 -0
  222. package/dist/schemas/skills.js +44 -0
  223. package/dist/schemas/users.js +28 -0
  224. package/dist/scripts/run-disposable-recovery-exercise.js +39 -0
  225. package/dist/sdk/index.js +2 -0
  226. package/dist/sdk/node-api-client.js +474 -0
  227. package/dist/security/approval-assertion.js +66 -0
  228. package/dist/security/command-completion-registry.js +35 -0
  229. package/dist/security/command-request.js +12 -0
  230. package/dist/security/command-result-signature.js +34 -0
  231. package/dist/security/managed-http.js +45 -0
  232. package/dist/security/native-completion-registry.js +44 -0
  233. package/dist/server.js +178 -0
  234. package/dist/services/admission-fence.js +71 -0
  235. package/dist/services/ai-assistant-service.js +2523 -0
  236. package/dist/services/ai-correlation-service.js +225 -0
  237. package/dist/services/ai-export-service.js +141 -0
  238. package/dist/services/ai-insights-service.js +219 -0
  239. package/dist/services/ai-schedule-service.js +145 -0
  240. package/dist/services/ai-scratchpad-service.js +222 -0
  241. package/dist/services/ai-tools.js +1343 -0
  242. package/dist/services/alert-service.js +313 -0
  243. package/dist/services/anomaly-detector.js +419 -0
  244. package/dist/services/api-local-work.js +79 -0
  245. package/dist/services/application-probes.js +309 -0
  246. package/dist/services/ardent-scope-enrollment.js +123 -0
  247. package/dist/services/ardent-service.js +86 -0
  248. package/dist/services/audit-service.js +308 -0
  249. package/dist/services/codex-service.js +209 -0
  250. package/dist/services/command-lifecycle.js +60 -0
  251. package/dist/services/command-resource-leases.js +305 -0
  252. package/dist/services/control-host-node.js +242 -0
  253. package/dist/services/credential-rotation-service.js +294 -0
  254. package/dist/services/dependency-service.js +145 -0
  255. package/dist/services/disposable-recovery-runner.js +595 -0
  256. package/dist/services/docker-group-health.js +121 -0
  257. package/dist/services/engineering-assurance.js +492 -0
  258. package/dist/services/engineering-evidence-ledger.js +94 -0
  259. package/dist/services/engineering-metric-evidence.js +26 -0
  260. package/dist/services/event-bus.js +227 -0
  261. package/dist/services/fleet-check-log.js +102 -0
  262. package/dist/services/forecast-service.js +154 -0
  263. package/dist/services/health-retention.js +175 -0
  264. package/dist/services/heartbeat-stream.js +63 -0
  265. package/dist/services/hermes-catalog-import.js +62 -0
  266. package/dist/services/hermes-cli-parser.js +671 -0
  267. package/dist/services/hermes-core-rollout.js +90 -0
  268. package/dist/services/hermes-dashboard-endpoint.js +13 -0
  269. package/dist/services/hermes-plugin-source.js +149 -0
  270. package/dist/services/hermes-registry-service.js +258 -0
  271. package/dist/services/hermes-service.js +566 -0
  272. package/dist/services/hivemem-client.js +85 -0
  273. package/dist/services/host-local-writer.js +82 -0
  274. package/dist/services/incident-evidence.js +110 -0
  275. package/dist/services/incident-lessons.js +186 -0
  276. package/dist/services/llm-providers/anthropic.js +143 -0
  277. package/dist/services/llm-providers/index.js +7 -0
  278. package/dist/services/llm-providers/llama-cpp.js +237 -0
  279. package/dist/services/llm-providers/ollama.js +292 -0
  280. package/dist/services/llm-providers/openai-compatible.js +200 -0
  281. package/dist/services/llm-providers/provider-error.js +36 -0
  282. package/dist/services/llm-providers/registry.js +45 -0
  283. package/dist/services/llm-providers/types.js +2 -0
  284. package/dist/services/log-emitter.js +19 -0
  285. package/dist/services/log-retention.js +66 -0
  286. package/dist/services/maintenance-service.js +29 -0
  287. package/dist/services/managed-cron.js +493 -0
  288. package/dist/services/managed-ssh-admission.js +68 -0
  289. package/dist/services/monitor-pulse.js +10 -0
  290. package/dist/services/node-issue-monitor.js +71 -0
  291. package/dist/services/node-service.js +35 -0
  292. package/dist/services/notification-retention.js +51 -0
  293. package/dist/services/notification-service.js +94 -0
  294. package/dist/services/operational-observations.js +112 -0
  295. package/dist/services/operations-config.js +67 -0
  296. package/dist/services/ops-service.js +491 -0
  297. package/dist/services/orchestra-extras-service.js +370 -0
  298. package/dist/services/orchestra-resources-service.js +530 -0
  299. package/dist/services/os-scheduler-writer.js +73 -0
  300. package/dist/services/password-service.js +22 -0
  301. package/dist/services/prediction-service.js +284 -0
  302. package/dist/services/recovery-exercises.js +682 -0
  303. package/dist/services/remediation-service.js +325 -0
  304. package/dist/services/resource-enrollment.js +433 -0
  305. package/dist/services/resource-node-lock.js +18 -0
  306. package/dist/services/resource-participant-work.js +302 -0
  307. package/dist/services/role-service.js +388 -0
  308. package/dist/services/rollout-service.js +440 -0
  309. package/dist/services/security-export.js +185 -0
  310. package/dist/services/security-fusion-actions.js +22 -0
  311. package/dist/services/security-fusion-approvals.js +83 -0
  312. package/dist/services/security-fusion-evidence-integrity.js +144 -0
  313. package/dist/services/security-fusion-ingest.js +101 -0
  314. package/dist/services/security-fusion-outbox.js +185 -0
  315. package/dist/services/security-fusion-receipt-chain.js +134 -0
  316. package/dist/services/security-fusion-service.js +751 -0
  317. package/dist/services/security-fusion-verification.js +50 -0
  318. package/dist/services/self-update-service.js +200 -0
  319. package/dist/services/shared-resource-leases.js +413 -0
  320. package/dist/services/skill-publication-bundle.js +90 -0
  321. package/dist/services/skill-publication-settlement.js +36 -0
  322. package/dist/services/skill-publication-state.js +40 -0
  323. package/dist/services/skill-registry-service.js +327 -0
  324. package/dist/services/skill-service.js +148 -0
  325. package/dist/services/ssh-exec-service.js +139 -0
  326. package/dist/services/ssh-host-verification.js +27 -0
  327. package/dist/services/tailscale-node-key-service.js +75 -0
  328. package/dist/services/team-approvals.js +118 -0
  329. package/dist/services/topology-service.js +138 -0
  330. package/dist/services/transfer-authority-snapshot.js +151 -0
  331. package/dist/services/webhook-credential-migration.js +97 -0
  332. package/dist/services/webhook-security.js +242 -0
  333. package/dist/services/webhook-service.js +494 -0
  334. package/dist/types/auth.js +3 -0
  335. package/dist/types/index.js +3 -0
  336. package/dist/types/library.js +3 -0
  337. package/dist/types/notifications.js +3 -0
  338. package/dist/types/roles.js +3 -0
  339. package/dist/types/shared-contracts.js +3 -0
  340. package/dist/utils/env.js +141 -0
  341. package/dist/utils/heartbeat-peer.js +19 -0
  342. package/dist/utils/idempotency.js +45 -0
  343. package/dist/utils/ip.js +25 -0
  344. package/dist/utils/json-fields.js +94 -0
  345. package/dist/utils/local-tls.js +39 -0
  346. package/dist/utils/magnus-home.js +15 -0
  347. package/dist/utils/node-url.js +59 -0
  348. package/dist/utils/redact.js +26 -0
  349. package/dist/utils/safe-equal.js +13 -0
  350. package/dist/utils/sentinel-nodes.js +16 -0
  351. package/dist/utils/validate.js +41 -0
  352. package/package.json +119 -4
  353. package/prisma/migrations/20261031000000_baseline/migration.sql +2935 -0
  354. package/prisma/migrations/migration_lock.toml +3 -0
  355. package/prisma/schema.prisma +2185 -0
  356. package/prisma.config.ts +14 -0
@@ -0,0 +1,121 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ function commandSucceeds(command, args) {
3
+ try {
4
+ execFileSync(command, args, { stdio: 'pipe', timeout: 3000, maxBuffer: 64 * 1024 });
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ function groupExists(groupName) {
12
+ return commandSucceeds('getent', ['group', groupName]);
13
+ }
14
+ function userExists(user) {
15
+ return commandSucceeds('id', ['-u', '--', user]);
16
+ }
17
+ function userInGroup(user, groupName) {
18
+ try {
19
+ const output = execFileSync('id', ['-nG', '--', user], { encoding: 'utf-8', timeout: 3000, maxBuffer: 64 * 1024 });
20
+ return output
21
+ .trim()
22
+ .split(/\s+/)
23
+ .filter(Boolean)
24
+ .includes(groupName);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Checks whether the configured deploy user can access Docker via group membership.
32
+ * Observation only: account and group changes are left to the operator.
33
+ */
34
+ export function checkDockerGroupHealthOnControlNode() {
35
+ const targetUser = process.env['MAGNUS_DOCKER_USER']?.trim() || null;
36
+ const targetUserSource = targetUser ? 'deploy-env' : 'unavailable';
37
+ const dockerInstalled = commandSucceeds('docker', ['--version']);
38
+ if (!dockerInstalled) {
39
+ return {
40
+ status: 'not-applicable',
41
+ dockerInstalled,
42
+ targetUser,
43
+ targetUserSource,
44
+ userExists: true,
45
+ inDockerGroup: false,
46
+ remediationAttempted: false,
47
+ remediationSucceeded: false,
48
+ requiresRecheck: false,
49
+ message: 'Docker not installed; docker group check skipped.',
50
+ };
51
+ }
52
+ if (!targetUser) {
53
+ return {
54
+ status: 'warning',
55
+ dockerInstalled,
56
+ targetUser,
57
+ targetUserSource,
58
+ userExists: false,
59
+ inDockerGroup: false,
60
+ remediationAttempted: false,
61
+ remediationSucceeded: false,
62
+ requiresRecheck: false,
63
+ message: 'Docker detected but MAGNUS_DOCKER_USER is not configured; cannot verify docker group membership.',
64
+ };
65
+ }
66
+ if (!userExists(targetUser)) {
67
+ return {
68
+ status: 'warning',
69
+ dockerInstalled,
70
+ targetUser,
71
+ targetUserSource,
72
+ userExists: false,
73
+ inDockerGroup: false,
74
+ remediationAttempted: false,
75
+ remediationSucceeded: false,
76
+ requiresRecheck: false,
77
+ message: `Docker detected but configured user '${targetUser}' does not exist on this host.`,
78
+ };
79
+ }
80
+ if (!groupExists('docker')) {
81
+ return {
82
+ status: 'warning',
83
+ dockerInstalled,
84
+ targetUser,
85
+ targetUserSource,
86
+ userExists: true,
87
+ inDockerGroup: false,
88
+ remediationAttempted: false,
89
+ remediationSucceeded: false,
90
+ requiresRecheck: false,
91
+ message: 'Docker detected but the docker group is unavailable. Operator permission via Telegram is required before changing groups.',
92
+ };
93
+ }
94
+ if (userInGroup(targetUser, 'docker')) {
95
+ return {
96
+ status: 'healthy',
97
+ dockerInstalled,
98
+ targetUser,
99
+ targetUserSource,
100
+ userExists: true,
101
+ inDockerGroup: true,
102
+ remediationAttempted: false,
103
+ remediationSucceeded: false,
104
+ requiresRecheck: false,
105
+ message: `Docker detected and user '${targetUser}' is already in docker group.`,
106
+ };
107
+ }
108
+ return {
109
+ status: 'warning',
110
+ dockerInstalled,
111
+ targetUser,
112
+ targetUserSource,
113
+ userExists: true,
114
+ inDockerGroup: false,
115
+ remediationAttempted: false,
116
+ remediationSucceeded: false,
117
+ requiresRecheck: false,
118
+ message: `Docker detected but docker group membership could not be verified for user '${targetUser}'. Operator permission via Telegram is required before changing membership.`,
119
+ };
120
+ }
121
+ //# sourceMappingURL=docker-group-health.js.map
@@ -0,0 +1,492 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, readFileSync } from 'node:fs';
3
+ import { z } from 'zod';
4
+ import { getDb } from '../db/index.js';
5
+ import { HttpError } from '../middleware/error-handler.js';
6
+ import { engineeringMetricEvidenceSha256 } from './engineering-metric-evidence.js';
7
+ import { assessRecoveryExercise } from './recovery-exercises.js';
8
+ const id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9:._/-]{0,159}$/);
9
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
10
+ const metricSourceSchema = z.discriminatedUnion('metric', [
11
+ z.object({ metric: z.literal('gpu_queue'), collector: z.literal('application_probe'), sourceKey: id }).strict(),
12
+ z.object({ metric: z.literal('dependency_latency'), collector: z.literal('application_probe'), sourceKey: id }).strict(),
13
+ z.object({ metric: z.literal('backup_age'), collector: z.literal('cron_success'), sourceKey: id }).strict(),
14
+ z.object({ metric: z.literal('certificate_expiry'), collector: z.literal('health_service_expiry'), sourceKey: id,
15
+ detailKey: z.literal('certificateExpiresAt') }).strict(),
16
+ z.object({ metric: z.literal('token_expiry'), collector: z.literal('health_service_expiry'), sourceKey: id,
17
+ detailKey: z.enum(['expiresAt', 'keyExpiry', 'renewalGrantExpiresAt']) }).strict(),
18
+ ]);
19
+ const applicationSchema = z.object({
20
+ id, nodeId: id,
21
+ deploySource: z.string().min(1).max(500),
22
+ runtimePath: z.string().regex(/^\/[a-zA-Z0-9._/@+-]+(?:\/[a-zA-Z0-9._@+-]+)*$/),
23
+ healthContract: z.object({
24
+ method: z.literal('GET'), path: z.string().regex(/^\/[a-zA-Z0-9/_.-]*$/),
25
+ successStatuses: z.array(z.number().int().min(200).max(399)).min(1).max(20),
26
+ maxAgeMs: z.number().int().min(1_000).max(86_400_000),
27
+ }).strict(),
28
+ dataOwner: id,
29
+ recoveryOrder: z.number().int().min(1).max(10_000),
30
+ dependsOn: z.array(id).max(100),
31
+ investigationSequence: z.array(z.string().min(1).max(500)).min(1).max(20),
32
+ recoverySequence: z.array(z.string().min(1).max(500)).min(1).max(20),
33
+ metricSources: z.array(metricSourceSchema).max(5).default([]),
34
+ }).strict().superRefine((value, ctx) => {
35
+ if (new Set(value.metricSources.map(source => source.metric)).size !== value.metricSources.length)
36
+ ctx.addIssue({ code: 'custom', message: 'Metric source must be unique per metric', path: ['metricSources'] });
37
+ });
38
+ const runbookSchema = z.object({
39
+ id, version: id, executionVersion: z.enum(['diagnostics.v1', 'restart-service.v1']),
40
+ kind: z.enum(['diagnostics', 'deploy', 'recovery', 'remediation', 'containment']),
41
+ mutatesState: z.boolean(),
42
+ timeoutSeconds: z.number().int().min(1).max(3_600),
43
+ maxAttempts: z.literal(1),
44
+ command: z.string().regex(/^\/[a-zA-Z0-9._/+@-]+(?:\/[a-zA-Z0-9._+@-]+)*$/),
45
+ args: z.array(z.string().max(500)).max(16),
46
+ preflight: z.array(z.string().min(1).max(500)).min(1).max(20),
47
+ steps: z.array(z.string().min(1).max(500)).min(1).max(30),
48
+ postconditions: z.array(z.string().min(1).max(500)).min(1).max(20),
49
+ directPostcondition: z.object({ kind: z.literal('application_health'), requirement: z.string().min(1).max(500) }).strict(),
50
+ rollbackCriteria: z.array(z.string().min(1).max(500)).min(1).max(20),
51
+ }).strict().superRefine((value, ctx) => {
52
+ if (value.postconditions.length !== 1 || value.postconditions[0] !== value.directPostcondition.requirement)
53
+ ctx.addIssue({ code: 'custom', message: 'Direct application health must cover the only postcondition', path: ['directPostcondition'] });
54
+ if (value.kind === 'diagnostics' && value.mutatesState)
55
+ ctx.addIssue({ code: 'custom', message: 'Diagnostics runbooks must be nonmutating', path: ['mutatesState'] });
56
+ if (value.kind !== 'diagnostics' && !value.mutatesState)
57
+ ctx.addIssue({ code: 'custom', message: 'Deploy, recovery, remediation, and containment runbooks must require mutation approval', path: ['mutatesState'] });
58
+ });
59
+ export const operationsCatalogSchema = z.object({
60
+ schemaVersion: z.literal(1), applications: z.array(applicationSchema).max(200),
61
+ runbooks: z.array(runbookSchema).max(100),
62
+ }).strict().superRefine((value, ctx) => {
63
+ const appIds = new Set(value.applications.map((app) => app.id));
64
+ if (appIds.size !== value.applications.length)
65
+ ctx.addIssue({ code: 'custom', message: 'Duplicate application id' });
66
+ if (new Set(value.runbooks.map((runbook) => `${runbook.id}@${runbook.version}`)).size !== value.runbooks.length)
67
+ ctx.addIssue({ code: 'custom', message: 'Duplicate runbook revision' });
68
+ if (new Set(value.applications.map((app) => app.recoveryOrder)).size !== value.applications.length)
69
+ ctx.addIssue({ code: 'custom', message: 'Recovery order must be unambiguous', path: ['applications'] });
70
+ const byId = new Map(value.applications.map((app) => [app.id, app]));
71
+ for (const app of value.applications)
72
+ for (const dependency of app.dependsOn) {
73
+ if (!appIds.has(dependency))
74
+ ctx.addIssue({ code: 'custom', message: `Unknown dependency ${dependency}`, path: ['applications'] });
75
+ if (dependency === app.id)
76
+ ctx.addIssue({ code: 'custom', message: 'Application cannot depend on itself', path: ['applications'] });
77
+ if ((byId.get(dependency)?.recoveryOrder ?? 0) >= app.recoveryOrder)
78
+ ctx.addIssue({ code: 'custom', message: `Dependency ${dependency} must recover before ${app.id}`, path: ['applications'] });
79
+ }
80
+ const dependencies = new Map(value.applications.map((app) => [app.id, app.dependsOn]));
81
+ const visiting = new Set();
82
+ const visited = new Set();
83
+ const visit = (applicationId) => {
84
+ if (visiting.has(applicationId))
85
+ return true;
86
+ if (visited.has(applicationId))
87
+ return false;
88
+ visiting.add(applicationId);
89
+ if ((dependencies.get(applicationId) ?? []).some(visit))
90
+ return true;
91
+ visiting.delete(applicationId);
92
+ visited.add(applicationId);
93
+ return false;
94
+ };
95
+ if (value.applications.some((app) => visit(app.id)))
96
+ ctx.addIssue({ code: 'custom', message: 'Dependency cycle detected', path: ['applications'] });
97
+ });
98
+ function canonical(value) {
99
+ if (Array.isArray(value))
100
+ return `[${value.map(canonical).join(',')}]`;
101
+ if (value && typeof value === 'object')
102
+ return `{${Object.entries(value)
103
+ .sort(([a], [b]) => a.localeCompare(b)).map(([key, field]) => `${JSON.stringify(key)}:${canonical(field)}`).join(',')}}`;
104
+ return JSON.stringify(value);
105
+ }
106
+ export function sha256(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
107
+ function catalogDigest(catalog) {
108
+ return sha256({ schemaVersion: catalog.schemaVersion, applications: catalog.applications, runbooks: catalog.runbooks });
109
+ }
110
+ export function loadOperationsCatalog(path = process.env['MAGNUS_OPERATIONS_CATALOG_FILE']) {
111
+ if (!path)
112
+ throw new Error('MAGNUS_OPERATIONS_CATALOG_FILE is not configured');
113
+ const stat = lstatSync(path);
114
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256_000 || (stat.mode & 0o022))
115
+ throw new Error('Unsafe operations catalog');
116
+ const parsed = operationsCatalogSchema.parse(JSON.parse(readFileSync(path, 'utf8')));
117
+ return { ...parsed, catalogSha256: catalogDigest(parsed) };
118
+ }
119
+ export function assertCatalogInventory(catalog, inventoryNodeIds) {
120
+ const missing = [...new Set(catalog.applications.map((app) => app.nodeId))].filter((nodeId) => !inventoryNodeIds.has(nodeId));
121
+ if (missing.length)
122
+ throw new HttpError(409, 'OPERATIONS_CATALOG_INVENTORY_DRIFT', `Operations catalog references nodes absent from Magnus inventory: ${missing.join(', ')}`);
123
+ }
124
+ export async function loadInventoryBoundOperationsCatalog() {
125
+ const catalog = loadOperationsCatalog();
126
+ const nodeIds = [...new Set(catalog.applications.map((app) => app.nodeId))];
127
+ const rows = await getDb().node.findMany({ where: { id: { in: nodeIds } }, select: { id: true }, take: 200 });
128
+ assertCatalogInventory(catalog, new Set(rows.map((row) => row.id)));
129
+ return catalog;
130
+ }
131
+ export const dependencyFailureSchema = z.object({
132
+ failedApplicationId: id,
133
+ }).strict();
134
+ function bindApplicationObservation(app, row, contract, now) {
135
+ let retained = null;
136
+ try {
137
+ retained = row ? JSON.parse(row.resultJson) : null;
138
+ }
139
+ catch {
140
+ retained = null;
141
+ }
142
+ const checks = retained && Array.isArray(retained.checks) ? retained.checks : [];
143
+ const health = checks.find((check) => Boolean(check && typeof check === 'object'
144
+ && !Array.isArray(check) && check['name'] === 'reachability'));
145
+ const httpStatus = health?.['httpStatus'];
146
+ const healthStatus = health?.['status'];
147
+ const statusContractValid = (typeof httpStatus === 'number' && Number.isInteger(httpStatus))
148
+ ? (app.healthContract.successStatuses.includes(httpStatus)
149
+ ? healthStatus === 'pass' || healthStatus === 'fail'
150
+ : healthStatus === 'fail')
151
+ : httpStatus === null && health?.['status'] === 'fail';
152
+ const probeContractValid = Boolean(contract && row
153
+ && contract.id === app.id && contract.nodeId === app.nodeId
154
+ && row.projectId === contract.projectId && row.serviceId === contract.serviceId
155
+ && row.nodeId === contract.nodeId && app.healthContract.method === 'GET'
156
+ && app.healthContract.path === contract.healthPath
157
+ && retained?.status === row.status && ['pass', 'fail', 'unknown', 'unconfigured'].includes(row.status)
158
+ && retained?.probeContractSha256 === contract.probeContractSha256
159
+ && health?.['method'] === app.healthContract.method && health?.['path'] === app.healthContract.path
160
+ && statusContractValid);
161
+ const durablyBound = Boolean(row && probeContractValid && row.observedAt
162
+ && row.observedAt.getTime() <= now + 30_000 && now - row.observedAt.getTime() <= app.healthContract.maxAgeMs);
163
+ const observedStatus = !durablyBound ? 'unknown' : row.status === 'pass' ? 'healthy' : row.status === 'fail' ? 'failed' : 'unknown';
164
+ return {
165
+ applicationId: app.id,
166
+ observedStatus,
167
+ observedAt: durablyBound ? row.observedAt.toISOString() : null,
168
+ releaseRevision: durablyBound && typeof retained?.releaseRevision === 'string' ? retained.releaseRevision : null,
169
+ evidenceSha256: durablyBound ? sha256({ id: row.id, projectId: row.projectId, serviceId: row.serviceId,
170
+ nodeId: row.nodeId, status: row.status, resultJson: row.resultJson, observedAt: row.observedAt.toISOString() }) : null,
171
+ durablyBound,
172
+ probeContractValid,
173
+ };
174
+ }
175
+ export function assessDependencyFailure(catalog, input, recordedObservations, probeContracts, now = Date.now()) {
176
+ const failed = catalog.applications.find((app) => app.id === input.failedApplicationId);
177
+ if (!failed)
178
+ throw new Error('Failed application is absent from the authoritative catalog');
179
+ const affected = new Set([failed.id]);
180
+ for (let changed = true; changed;) {
181
+ changed = false;
182
+ for (const app of catalog.applications)
183
+ if (!affected.has(app.id) && app.dependsOn.some((dep) => affected.has(dep))) {
184
+ affected.add(app.id);
185
+ changed = true;
186
+ }
187
+ }
188
+ const applications = catalog.applications.filter((app) => affected.has(app.id)).sort((a, b) => a.recoveryOrder - b.recoveryOrder);
189
+ const observations = new Map(recordedObservations.map((row) => [row.id, row]));
190
+ const contracts = new Map(probeContracts.map((contract) => [contract.id, contract]));
191
+ const evidenceRows = applications.map((app) => bindApplicationObservation(app, observations.get(app.id), contracts.get(app.id), now));
192
+ const evidenceByApplication = new Map(evidenceRows.map((row) => [row.applicationId, row]));
193
+ const failedEvidence = evidenceByApplication.get(failed.id);
194
+ const failureDurablyObserved = failedEvidence.durablyBound && failedEvidence.observedStatus === 'failed';
195
+ const durableObservationsComplete = evidenceRows.every((row) => row.durablyBound && row.observedStatus !== 'unknown');
196
+ return {
197
+ schemaVersion: 1, catalogSha256: catalogDigest(catalog), failedApplicationId: failed.id,
198
+ affectedApplications: applications.map((app) => ({ id: app.id, nodeId: app.nodeId, deploySource: app.deploySource, runtimePath: app.runtimePath,
199
+ dataOwner: app.dataOwner, recoveryOrder: app.recoveryOrder, healthContract: app.healthContract,
200
+ observedStatus: evidenceByApplication.get(app.id).observedStatus,
201
+ observedAt: evidenceByApplication.get(app.id).observedAt,
202
+ evidenceSha256: evidenceByApplication.get(app.id).evidenceSha256,
203
+ durablyBound: evidenceByApplication.get(app.id).durablyBound,
204
+ probeContractValid: evidenceByApplication.get(app.id).probeContractValid })),
205
+ investigationSequence: applications.flatMap((app) => app.investigationSequence.map((step) => ({ applicationId: app.id, step }))),
206
+ recoverySequence: applications.flatMap((app) => app.recoverySequence.map((step) => ({ applicationId: app.id, step }))),
207
+ provenance: 'magnus_application_observations',
208
+ failureDurablyObserved, durableObservationsComplete,
209
+ };
210
+ }
211
+ export const runbookAssessmentSchema = z.object({
212
+ runbookId: id, runbookVersion: id, applicationId: id, planSha256: digest, leaseId: id,
213
+ approvalExecutionId: id.optional(),
214
+ }).strict();
215
+ export function catalogRunbookPlan(catalog, runbookId, runbookVersion, applicationId) {
216
+ const runbook = catalog.runbooks.find(candidate => candidate.id === runbookId && candidate.version === runbookVersion);
217
+ const target = catalog.applications.find(candidate => candidate.id === applicationId);
218
+ if (!runbook || !target)
219
+ return null;
220
+ return { runbookId: runbook.id, runbookVersion: runbook.version, executionVersion: runbook.executionVersion,
221
+ applicationId, nodeId: target.nodeId, kind: runbook.kind, mutatesState: runbook.mutatesState,
222
+ command: runbook.command, args: runbook.args, timeoutSeconds: runbook.timeoutSeconds, maxAttempts: runbook.maxAttempts,
223
+ preflight: runbook.preflight, steps: runbook.steps, postconditions: runbook.postconditions,
224
+ directPostcondition: runbook.directPostcondition, rollbackCriteria: runbook.rollbackCriteria };
225
+ }
226
+ export function catalogRunbookPlanSha256(catalog, runbookId, runbookVersion, applicationId) {
227
+ const plan = catalogRunbookPlan(catalog, runbookId, runbookVersion, applicationId);
228
+ return plan ? sha256(plan) : null;
229
+ }
230
+ export function assessRunbook(catalog, input, recordedApproval, recordedLease, executionBinding, recordedObservation, probeContract, now = Date.now()) {
231
+ const runbook = catalog.runbooks.find((r) => r.id === input.runbookId && r.version === input.runbookVersion);
232
+ if (!runbook)
233
+ throw new Error('Runbook revision is not approved in the authoritative catalog');
234
+ if (!catalog.applications.some((app) => app.id === input.applicationId))
235
+ throw new Error('Runbook target is absent from the authoritative catalog');
236
+ const target = catalog.applications.find((app) => app.id === input.applicationId);
237
+ const catalogPlanSha256 = catalogRunbookPlanSha256(catalog, runbook.id, runbook.version, input.applicationId);
238
+ const actionAt = recordedLease?.dispatchedAt?.getTime() ?? Number.NaN;
239
+ const catalogBindingValid = Boolean(executionBinding && recordedLease && executionBinding.commandId === recordedLease.commandId
240
+ && executionBinding.nodeId === target.nodeId && executionBinding.applicationId === target.id
241
+ && executionBinding.runbookVersion === runbook.executionVersion && executionBinding.command === runbook.command
242
+ && JSON.stringify(executionBinding.args) === JSON.stringify(runbook.args)
243
+ && executionBinding.timeoutSeconds === runbook.timeoutSeconds);
244
+ const expectedPlanSha256 = executionBinding?.catalogPlanSha256 ?? null;
245
+ const approvalValid = !runbook.mutatesState || Boolean(input.approvalExecutionId && recordedApproval && executionBinding
246
+ && recordedApproval.executionId === input.approvalExecutionId && recordedApproval.decision === 'approve'
247
+ && executionBinding.telegramExecutionId === input.approvalExecutionId && executionBinding.decisionId === recordedApproval.decisionId
248
+ && recordedApproval.planSha256 === executionBinding.approvalPlanSha256 && recordedApproval.decidedAt.getTime() <= actionAt
249
+ && recordedApproval.expiresAt.getTime() > actionAt);
250
+ const leaseValid = Boolean(recordedLease && recordedLease.id === input.leaseId && recordedLease.nodeId === target.nodeId
251
+ && recordedLease.commandId === executionBinding?.commandId && recordedLease.state === 'released'
252
+ && recordedLease.acquiredAt.getTime() <= actionAt && recordedLease.deadline.getTime() >= actionAt
253
+ && recordedLease.dispatchedAt && recordedLease.dispatchedAt.getTime() <= actionAt
254
+ && recordedLease.settledAt && recordedLease.settledAt.getTime() >= actionAt
255
+ && recordedLease.evidenceSha256 !== null && digest.safeParse(recordedLease.evidenceSha256).success);
256
+ const catalogPlanDigestValid = input.planSha256 === catalogPlanSha256 && expectedPlanSha256 === catalogPlanSha256;
257
+ const executionDurablyBound = catalogPlanDigestValid && catalogBindingValid && approvalValid && leaseValid;
258
+ const applicationEvidence = bindApplicationObservation(target, recordedObservation, probeContract, now);
259
+ const directPostconditionVerified = Boolean(runbook.directPostcondition.kind === 'application_health'
260
+ && applicationEvidence.durablyBound && applicationEvidence.observedStatus === 'healthy'
261
+ && recordedLease?.settledAt && applicationEvidence.observedAt
262
+ && Date.parse(applicationEvidence.observedAt) >= recordedLease.settledAt.getTime());
263
+ const verified = executionDurablyBound && directPostconditionVerified;
264
+ return { schemaVersion: 2, verified, provenance: 'durable_execution_and_application_probe',
265
+ executionDurablyBound, directPostconditionVerified, expectedPlanSha256, catalogPlanSha256,
266
+ catalogPlanDigestValid, catalogBindingValid, approvalValid, leaseValid, noBlindRetry: runbook.maxAttempts === 1,
267
+ directPostcondition: runbook.directPostcondition, applicationEvidence,
268
+ reason: verified ? null : 'Plan, permission, settled lease, or direct postcondition evidence is incomplete' };
269
+ }
270
+ export const deliveryAssessmentSchema = z.object({
271
+ requestId: id,
272
+ }).strict();
273
+ export function assessDelivery(catalog, input, record, canary, recordedObservation, probeContract, ledgerSha256, now = Date.now()) {
274
+ if (!record || record.requestId !== input.requestId)
275
+ return { schemaVersion: 2, verified: false,
276
+ provenance: 'pinned_engineering_evidence_ledger', requestId: input.requestId,
277
+ reason: 'No direct delivery evidence receipt exists for this request', ledgerSha256 };
278
+ const application = catalog.applications.find(candidate => candidate.id === record.applicationId);
279
+ if (!application)
280
+ throw new Error('Delivery application is absent from the authoritative catalog');
281
+ const revision = record.source.workingRevision;
282
+ const deploySourceValid = record.deploySource === application.deploySource;
283
+ const revisionsMatch = record.source.mainRevision === revision && record.source.remoteRevision === revision
284
+ && record.ci.revision === revision && record.review.revision === revision
285
+ && record.tests.every(test => test.revision === revision)
286
+ && (!record.deployment || record.deployment.revision === revision);
287
+ const testTimes = record.tests.map(test => Date.parse(test.observedAt));
288
+ const reviewAt = Date.parse(record.review.observedAt);
289
+ const ciAt = Date.parse(record.ci.observedAt);
290
+ const receiptAt = Date.parse(record.recordedAt);
291
+ const chronologyValid = [...testTimes, reviewAt, ciAt, receiptAt].every(timestamp => timestamp <= now + 30_000)
292
+ && testTimes.every(timestamp => timestamp <= reviewAt && timestamp <= ciAt)
293
+ && reviewAt <= receiptAt && ciAt <= receiptAt
294
+ && (!record.deployment || Date.parse(record.deployment.deployedAt) <= receiptAt);
295
+ const deploymentRequired = record.establishedProductionWorkflow && !record.documentationOnly;
296
+ const deploymentIdentityValid = Boolean(record.deployment
297
+ && record.deployment.applicationId === application.id
298
+ && record.deployment.deploySource === application.deploySource
299
+ && record.deployment.sourceTreeSha256 === record.source.sourceTreeSha256
300
+ && Date.parse(record.deployment.deployedAt) <= now + 30_000);
301
+ const canaryValid = Boolean(canary && canary.acceptedCommit === revision
302
+ && canary.sourceTreeSha256 === record.source.sourceTreeSha256
303
+ && canary.createdAt.getTime() >= reviewAt && canary.createdAt.getTime() >= ciAt
304
+ && canary.createdAt.getTime() <= now + 30_000
305
+ && record.deployment && canary.createdAt.getTime() <= Date.parse(record.deployment.deployedAt));
306
+ const applicationEvidence = bindApplicationObservation(application, recordedObservation, probeContract, now);
307
+ const postDeployHealthValid = Boolean(canaryValid && applicationEvidence.durablyBound
308
+ && applicationEvidence.observedStatus === 'healthy' && applicationEvidence.releaseRevision === revision
309
+ && applicationEvidence.observedAt && record.deployment
310
+ && Date.parse(applicationEvidence.observedAt) >= Date.parse(record.deployment.deployedAt));
311
+ const deploymentVerified = deploymentRequired ? deploymentIdentityValid && canaryValid && postDeployHealthValid : true;
312
+ const verified = deploySourceValid && revisionsMatch && chronologyValid && deploymentVerified;
313
+ return { schemaVersion: 2, verified, provenance: 'pinned_engineering_evidence_ledger',
314
+ requestId: record.requestId, applicationId: record.applicationId, revision, revisionsMatch, chronologyValid,
315
+ deploySourceValid, deploymentRequired, deploymentIdentityValid, deploymentVerified, canaryValid, postDeployHealthValid, applicationEvidence,
316
+ receiptSha256: record.receiptSha256, ledgerSha256, limitations: record.limitations,
317
+ reason: verified ? null : 'Repository, tests, review, CI, release canary, or post-deploy health evidence is incomplete' };
318
+ }
319
+ export const forecastAssessmentSchema = z.object({
320
+ applicationId: id,
321
+ metric: z.enum(['disk', 'memory', 'gpu_queue', 'backup_age', 'certificate_expiry', 'token_expiry', 'dependency_latency']),
322
+ diskMountPoint: z.string().regex(/^\/(?:[a-zA-Z0-9._@+-]+(?:\/[a-zA-Z0-9._@+-]+)*)?$/).max(500)
323
+ .refine(value => !value.split('/').some(segment => segment === '..'), 'diskMountPoint cannot traverse').optional(),
324
+ threshold: z.number().finite().min(0).max(1_000_000_000_000), breachDirection: z.enum(['above', 'below']),
325
+ maxSampleAgeMs: z.number().int().min(60_000).max(2_592_000_000),
326
+ maxWindowMs: z.number().int().min(60_000).max(31_536_000_000),
327
+ }).strict().superRefine((value, ctx) => {
328
+ if (value.metric === 'disk' && !value.diskMountPoint)
329
+ ctx.addIssue({ code: 'custom', message: 'diskMountPoint is required for disk forecasts', path: ['diskMountPoint'] });
330
+ if (value.metric === 'memory' && value.diskMountPoint)
331
+ ctx.addIssue({ code: 'custom', message: 'diskMountPoint is only valid for disk forecasts', path: ['diskMountPoint'] });
332
+ if (!['disk', 'memory'].includes(value.metric) && value.diskMountPoint)
333
+ ctx.addIssue({ code: 'custom', message: 'diskMountPoint is only valid for disk forecasts', path: ['diskMountPoint'] });
334
+ if (['disk', 'memory'].includes(value.metric) && value.threshold > 100)
335
+ ctx.addIssue({ code: 'custom', message: 'Percent thresholds cannot exceed 100', path: ['threshold'] });
336
+ });
337
+ function assessDurableForecast(input, samples, evidenceWindowComplete, unit, provenance, now = Date.now()) {
338
+ if (samples.length < 3)
339
+ return {
340
+ schemaVersion: 3, applicationId: input.applicationId, metric: input.metric, unit,
341
+ diskMountPoint: input.diskMountPoint ?? null, sampleCount: samples.length, window: null,
342
+ trendPerDay: null, confidence: null, timestampsValid: false, sustainedBreach: false,
343
+ evidenceWindowComplete, durablyMeasured: false, provenance,
344
+ candidateAlert: false, evidenceSetSha256: sha256(samples),
345
+ reason: 'At least three durable Magnus source samples are required',
346
+ };
347
+ const t0 = Date.parse(samples[0].observedAt);
348
+ const day = 86_400_000;
349
+ const xs = samples.map((s) => (Date.parse(s.observedAt) - t0) / day);
350
+ const ys = samples.map((s) => s.value);
351
+ const mx = xs.reduce((a, b) => a + b, 0) / xs.length;
352
+ const my = ys.reduce((a, b) => a + b, 0) / ys.length;
353
+ const denominator = xs.reduce((sum, x) => sum + (x - mx) ** 2, 0);
354
+ const slope = denominator ? xs.reduce((sum, x, index) => sum + (x - mx) * (ys[index] - my), 0) / denominator : 0;
355
+ const residual = ys.reduce((sum, y, index) => sum + (y - (my + slope * (xs[index] - mx))) ** 2, 0);
356
+ const total = ys.reduce((sum, y) => sum + (y - my) ** 2, 0);
357
+ const confidence = total === 0 ? 1 : Math.max(0, Math.min(1, 1 - residual / total));
358
+ const breaches = samples.slice(-3).map((s) => input.breachDirection === 'above' ? s.value >= input.threshold : s.value <= input.threshold);
359
+ const latestAt = Date.parse(samples.at(-1).observedAt);
360
+ const timestampsValid = samples.every((sample, index) => Date.parse(sample.observedAt) <= now
361
+ && (index === 0 || Date.parse(sample.observedAt) > Date.parse(samples[index - 1].observedAt)))
362
+ && latestAt >= now - input.maxSampleAgeMs
363
+ && Date.parse(samples[0].observedAt) >= now - input.maxWindowMs
364
+ && latestAt - Date.parse(samples[0].observedAt) <= input.maxWindowMs;
365
+ const durablyMeasured = timestampsValid && evidenceWindowComplete;
366
+ return { schemaVersion: 3, applicationId: input.applicationId, metric: input.metric, unit,
367
+ diskMountPoint: input.diskMountPoint ?? null, sampleCount: samples.length,
368
+ window: { start: samples[0].observedAt, end: samples.at(-1).observedAt }, trendPerDay: slope,
369
+ confidence, timestampsValid, sustainedBreach: breaches.length === 3 && breaches.every(Boolean),
370
+ evidenceWindowComplete, durablyMeasured, provenance,
371
+ candidateAlert: durablyMeasured && breaches.length === 3 && breaches.every(Boolean),
372
+ evidenceSetSha256: sha256(samples), reason: durablyMeasured ? null : 'Source evidence is stale, unordered, or truncated' };
373
+ }
374
+ export function assessForecast(catalog, input, evidenceRows, evidenceWindowComplete = true, now = Date.now()) {
375
+ const application = catalog.applications.find((app) => app.id === input.applicationId);
376
+ if (!application)
377
+ throw new Error('Forecast target is absent from the authoritative catalog');
378
+ const source = application.metricSources.find(candidate => candidate.metric === input.metric);
379
+ if (!['disk', 'memory'].includes(input.metric) && !source)
380
+ return {
381
+ schemaVersion: 3, applicationId: input.applicationId, nodeId: application.nodeId, metric: input.metric,
382
+ unit: null, sampleCount: 0, window: null, trendPerDay: null, confidence: null, timestampsValid: false,
383
+ sustainedBreach: false, evidenceWindowComplete, durablyMeasured: false, candidateAlert: false,
384
+ provenance: 'unconfigured', evidenceSetSha256: sha256([]), reason: 'No authoritative metric source is configured',
385
+ };
386
+ const byTimestamp = new Map();
387
+ for (const row of [...(evidenceRows.healthSnapshots ?? [])].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime() || a.id.localeCompare(b.id))) {
388
+ if (row.nodeId === application.nodeId)
389
+ byTimestamp.set(row.timestamp.getTime(), row);
390
+ }
391
+ const samples = [];
392
+ if (input.metric === 'disk' || input.metric === 'memory')
393
+ for (const row of byTimestamp.values()) {
394
+ let value = null;
395
+ if (input.metric === 'memory' && Number.isFinite(row.memoryPercent) && row.memoryPercent >= 0 && row.memoryPercent <= 100)
396
+ value = row.memoryPercent;
397
+ if (input.metric === 'disk') {
398
+ try {
399
+ const disks = JSON.parse(row.disks);
400
+ if (Array.isArray(disks)) {
401
+ const disk = disks.find((item) => Boolean(item && typeof item === 'object'
402
+ && !Array.isArray(item) && item['mountPoint'] === input.diskMountPoint));
403
+ const usage = disk?.['usagePercent'];
404
+ if (typeof usage === 'number' && Number.isFinite(usage) && usage >= 0 && usage <= 100)
405
+ value = usage;
406
+ }
407
+ }
408
+ catch { /* malformed retained telemetry is omitted and cannot become evidence */ }
409
+ }
410
+ if (value === null)
411
+ continue;
412
+ const observedAt = row.timestamp.toISOString();
413
+ samples.push({ observedAt, value, source: 'magnus-health-snapshot',
414
+ evidenceSha256: sha256({ id: row.id, nodeId: row.nodeId, timestamp: observedAt,
415
+ memoryPercent: row.memoryPercent, disks: row.disks, source: row.source }) });
416
+ }
417
+ if (source?.collector === 'application_probe') {
418
+ const expectedUnit = input.metric === 'gpu_queue' ? 'jobs' : 'ms';
419
+ const currentContract = evidenceRows.probeContract;
420
+ const expectedCheck = currentContract?.checks?.find(check => check.sourceKey === source.sourceKey);
421
+ const rows = [...(evidenceRows.metricSamples ?? [])].sort((a, b) => a.observedAt.getTime() - b.observedAt.getTime() || a.id.localeCompare(b.id));
422
+ const direct = new Map();
423
+ for (const row of rows)
424
+ if (row.applicationId === application.id && row.nodeId === application.nodeId
425
+ && row.metric === input.metric && row.sourceKey === source.sourceKey && row.unit === expectedUnit
426
+ && Number.isFinite(row.value) && row.value >= 0 && currentContract?.id === application.id
427
+ && currentContract.nodeId === application.nodeId && row.probeContractSha256 === currentContract.probeContractSha256
428
+ && expectedCheck && row.method === expectedCheck.method && row.path === expectedCheck.path
429
+ && Number.isInteger(row.httpStatus) && row.httpStatus >= 200 && row.httpStatus <= 399
430
+ && engineeringMetricEvidenceSha256(row) === row.evidenceSha256)
431
+ direct.set(row.observedAt.getTime(), row);
432
+ for (const row of direct.values())
433
+ samples.push({ observedAt: row.observedAt.toISOString(), value: row.value,
434
+ evidenceSha256: row.evidenceSha256, source: `application-probe:${source.sourceKey}` });
435
+ }
436
+ if (source?.collector === 'health_service_expiry')
437
+ for (const row of byTimestamp.values()) {
438
+ try {
439
+ const services = JSON.parse(row.services ?? '[]');
440
+ if (!Array.isArray(services))
441
+ continue;
442
+ const service = services.find((item) => Boolean(item && typeof item === 'object'
443
+ && !Array.isArray(item) && item['name'] === source.sourceKey));
444
+ const details = typeof service?.['details'] === 'string' ? JSON.parse(service['details']) : service?.['details'];
445
+ if (!details || typeof details !== 'object' || Array.isArray(details))
446
+ continue;
447
+ const expiry = details[source.detailKey];
448
+ if (typeof expiry !== 'string')
449
+ continue;
450
+ const expiresAt = Date.parse(expiry);
451
+ if (!Number.isFinite(expiresAt))
452
+ continue;
453
+ samples.push({ observedAt: row.timestamp.toISOString(), value: Math.max(0, (expiresAt - row.timestamp.getTime()) / 86_400_000),
454
+ source: `health-service:${source.sourceKey}:${source.detailKey}`,
455
+ evidenceSha256: sha256({ id: row.id, nodeId: row.nodeId, timestamp: row.timestamp.toISOString(),
456
+ services: row.services, source: row.source, sourceKey: source.sourceKey, detailKey: source.detailKey }) });
457
+ }
458
+ catch { /* malformed retained service telemetry is omitted */ }
459
+ }
460
+ if (source?.collector === 'cron_success') {
461
+ const executions = [...(evidenceRows.cronExecutions ?? [])].filter(row => row.cronJobId === source.sourceKey
462
+ && row.nodeId === application.nodeId && row.exitCode === 0 && row.completedAt)
463
+ .sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime());
464
+ let executionIndex = 0;
465
+ let latest;
466
+ for (const row of byTimestamp.values()) {
467
+ while (executionIndex < executions.length
468
+ && executions[executionIndex].completedAt.getTime() <= row.timestamp.getTime())
469
+ latest = executions[executionIndex++];
470
+ if (!latest)
471
+ continue;
472
+ samples.push({ observedAt: row.timestamp.toISOString(), value: (row.timestamp.getTime() - latest.completedAt.getTime()) / 86_400_000,
473
+ source: `cron-success:${source.sourceKey}`,
474
+ evidenceSha256: sha256({ healthSnapshotId: row.id, cronExecutionId: latest.id, nodeId: row.nodeId,
475
+ timestamp: row.timestamp.toISOString(), completedAt: latest.completedAt.toISOString(), exitCode: latest.exitCode }) });
476
+ }
477
+ }
478
+ const unit = input.metric === 'disk' || input.metric === 'memory' ? 'percent'
479
+ : input.metric === 'gpu_queue' ? 'jobs' : input.metric === 'dependency_latency' ? 'ms' : 'days';
480
+ const provenance = input.metric === 'disk' || input.metric === 'memory' ? 'magnus_health_snapshots'
481
+ : source?.collector === 'application_probe' ? 'magnus_engineering_metric_samples'
482
+ : source?.collector === 'cron_success' ? 'magnus_cron_execution_history' : 'magnus_health_service_snapshots';
483
+ return { ...assessDurableForecast(input, samples, evidenceWindowComplete, unit, provenance, now),
484
+ nodeId: application.nodeId };
485
+ }
486
+ export const restoreAssessmentSchema = z.object({
487
+ exerciseId: id,
488
+ }).strict();
489
+ export function assessRestore(input, record, productionIdentityFound, now = Date.now(), assessmentInventorySnapshotSha256 = null) {
490
+ return assessRecoveryExercise(input.exerciseId, record, productionIdentityFound, now, assessmentInventorySnapshotSha256);
491
+ }
492
+ //# sourceMappingURL=engineering-assurance.js.map