@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,302 @@
1
+ /** Durable participant work is coordination, never execution approval.
2
+ * Principals are fixed by server imports; no HTTP caller chooses a participant.
3
+ * SSH exit status alone cannot release running work or prove descendant death.
4
+ */
5
+ import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
6
+ import { getDb } from '../db/index.js';
7
+ import { HttpError } from '../middleware/error-handler.js';
8
+ import { lockResourceNode, resourceNodeIdentity as identity } from './resource-node-lock.js';
9
+ import { assertNodeAdmissionOpen } from './admission-fence.js';
10
+ import { participantWorkReconciliationSchema, participantWorkReconciliationSigningPayload, participantWorkRequestDigest, participantWorkKindSchema, historicalParticipantWorkReconciliationSchema, historicalParticipantWorkSigningPayload, } from '../schemas/resource-participant-reconciliation.js';
11
+ import { canonicalNodeSessionReceipt, nodeSessionReceiptSchema, verifyNodeSessionReceiptSignature, } from '../schemas/node-session-receipt.js';
12
+ import { RESULT_SIGNING_PREVIOUS_SECRET, RESULT_SIGNING_SECRET } from '../utils/env.js';
13
+ import { COMMAND_COMPLETION_REGISTRY, COMMAND_COMPLETION_REGISTRY_SHA256 } from '../security/command-completion-registry.js';
14
+ function fail(code, message) { throw new HttpError(409, code, message); }
15
+ const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
16
+ const PARTICIPANT_KINDS = {
17
+ api: new Set(['ssh-command', 'agent-deploy', 'agent-upgrade', 'control-deploy', 'agent-migrate',
18
+ 'self-update-check', 'self-update-apply', 'self-update-restart']),
19
+ dashboard: new Set(['terminal-http', 'terminal-ws', 'hermes-http', 'hermes-ws']),
20
+ };
21
+ function workFields(row) {
22
+ return { workId: row.workId, nodeId: row.nodeId, nodeFqdn: row.nodeFqdn, participant: row.participant, ownerId: row.ownerId, kind: row.kind,
23
+ state: row.state, epoch: row.epoch, barrierId: row.barrierId, requestSha256: row.requestSha256,
24
+ operationSha256: row.operationSha256, fence: row.fence?.toString() ?? null, deadline: row.deadline?.getTime() ?? null };
25
+ }
26
+ function participantWork(participant) {
27
+ async function reserve(input) {
28
+ if (!participantWorkKindSchema.safeParse(input.kind).success || !PARTICIPANT_KINDS[participant].has(input.kind)
29
+ || !/^[a-f0-9]{64}$/.test(input.operationSha256)
30
+ || !Number.isSafeInteger(input.deadline) || input.deadline <= Date.now() || input.deadline > Date.now() + 3_600_000)
31
+ fail('RESOURCE_WORK_INVALID', 'Exact bounded participant authority is required');
32
+ const nodes = await getDb().node.findMany({ where: { OR: [{ hostname: input.host.toLowerCase() }, { ipAddress: input.host.toLowerCase() }] }, select: { id: true }, take: 2 });
33
+ if (nodes.length !== 1)
34
+ fail('RESOURCE_TARGET_INVALID', 'Exact unambiguous inventory target required');
35
+ const nodeId = nodes[0].id;
36
+ return getDb().$transaction(async (tx) => {
37
+ await lockResourceNode(tx, nodeId);
38
+ const { node, row } = await identity(tx, nodeId);
39
+ await assertNodeAdmissionOpen(tx, nodeId);
40
+ const targets = await tx.node.findMany({ where: { OR: [{ hostname: input.host.toLowerCase() }, { ipAddress: input.host.toLowerCase() }] }, select: { id: true }, take: 2 });
41
+ if (targets.length !== 1 || targets[0]?.id !== nodeId)
42
+ fail('RESOURCE_TARGET_INVALID', 'Target changed or became ambiguous');
43
+ const history = await tx.commandResourceLease.findFirst({ where: { nodeId }, select: { id: true } });
44
+ if (row?.state === 'draining' || ((process.env['MAGNUS_RESOURCE_LEASES_ENABLED'] === 'true' || history) && row?.state !== 'enrolled'))
45
+ fail('RESOURCE_DRAIN_REQUIRED', 'New sessions are barred during enrollment');
46
+ // Enrolled SSH still needs descendant settlement/supervision integration.
47
+ if (row?.state === 'enrolled')
48
+ fail('RESOURCE_SSH_UNAVAILABLE', 'Enrolled SSH descendants require managed completion evidence');
49
+ const existing = await tx.resourceParticipantWork.findUnique({ where: { workId: input.workId } });
50
+ if (existing) {
51
+ if (existing.participant !== participant || existing.nodeId !== nodeId
52
+ || existing.ownerId !== input.ownerId || existing.kind !== input.kind
53
+ || existing.operationSha256 !== input.operationSha256 || existing.deadline?.getTime() !== input.deadline
54
+ || existing.fence === null || existing.requestSha256 === null)
55
+ fail('RESOURCE_WORK_CONFLICT', 'Work identity already used');
56
+ return workFields(existing);
57
+ }
58
+ const resourceKey = `participant:${participant}:${nodeId}`;
59
+ const allocated = await tx.commandResourceFence.upsert({ where: { resourceKey },
60
+ create: { resourceKey, generation: 1n }, update: { generation: { increment: 1 } }, select: { generation: true } });
61
+ const fence = allocated.generation.toString();
62
+ const requestSha256 = participantWorkRequestDigest({ participant, workId: input.workId, nodeId, nodeFqdn: node.hostname,
63
+ ownerId: input.ownerId, kind: input.kind, epoch: row?.epoch ?? 0, barrierId: row?.barrierId ?? null,
64
+ operationSha256: input.operationSha256, fence, deadline: input.deadline });
65
+ const work = await tx.resourceParticipantWork.create({ data: { workId: input.workId, ownerId: input.ownerId,
66
+ nodeId, nodeFqdn: node.hostname, participant, kind: input.kind, epoch: row?.epoch ?? 0, barrierId: row?.barrierId ?? null,
67
+ operationSha256: input.operationSha256, fence: allocated.generation, deadline: new Date(input.deadline), requestSha256 } });
68
+ return workFields(work);
69
+ });
70
+ }
71
+ async function start(workId, input) {
72
+ const db = getDb();
73
+ const found = await db.resourceParticipantWork.findUnique({ where: { workId } });
74
+ if (!found)
75
+ throw new HttpError(404, 'RESOURCE_WORK_NOT_FOUND', 'Work not found');
76
+ const result = await db.$transaction(async (tx) => {
77
+ await lockResourceNode(tx, found.nodeId);
78
+ const { node, row } = await identity(tx, found.nodeId);
79
+ const work = await tx.resourceParticipantWork.findUniqueOrThrow({ where: { workId } });
80
+ if (work.ownerId !== input.ownerId || work.participant !== participant || work.state !== 'reserved')
81
+ fail('RESOURCE_WORK_CONFLICT', 'Start is a single-use owner grant');
82
+ const history = await tx.commandResourceLease.findFirst({ where: { nodeId: node.id }, select: { id: true } });
83
+ const targets = input.targetHost === undefined ? null : await tx.node.findMany({ where: { OR: [
84
+ { hostname: input.targetHost.toLowerCase() }, { ipAddress: input.targetHost.toLowerCase() }
85
+ ] }, select: { id: true }, take: 2 });
86
+ const admissionFenced = Boolean(await tx.admissionFence.findUnique({ where: { nodeId: node.id }, select: { nodeId: true } }));
87
+ if (admissionFenced || (targets && (targets.length !== 1 || targets[0]?.id !== node.id))
88
+ || row?.state === 'draining' || row?.state === 'enrolled' || history || process.env['MAGNUS_RESOURCE_LEASES_ENABLED'] === 'true'
89
+ || input.epoch !== work.epoch || input.barrierId !== work.barrierId
90
+ || input.operationSha256 !== work.operationSha256 || input.fence !== work.fence?.toString()
91
+ || input.deadline !== work.deadline?.getTime() || input.requestSha256 !== work.requestSha256
92
+ || !work.deadline || work.deadline.getTime() <= Date.now()
93
+ || (row?.epoch ?? 0) !== work.epoch || (row?.barrierId ?? null) !== work.barrierId) {
94
+ await tx.resourceParticipantWork.update({ where: { workId }, data: { state: 'released', settledAt: new Date(),
95
+ settlement: 'barrier-before-dispatch', evidenceSha256: digest({ workId, barrier: row?.barrierId ?? null }) } });
96
+ return null;
97
+ }
98
+ return workFields(await tx.resourceParticipantWork.update({ where: { workId }, data: { state: 'running', startedAt: new Date() } }));
99
+ });
100
+ if (!result)
101
+ fail('RESOURCE_DRAIN_REQUIRED', 'Drain barrier changed before session dispatch');
102
+ return result;
103
+ }
104
+ async function finish(workId, input) {
105
+ const db = getDb();
106
+ const found = await db.resourceParticipantWork.findUnique({ where: { workId } });
107
+ if (!found)
108
+ throw new HttpError(404, 'RESOURCE_WORK_NOT_FOUND', 'Work not found');
109
+ return db.$transaction(async (tx) => {
110
+ await lockResourceNode(tx, found.nodeId);
111
+ const work = await tx.resourceParticipantWork.findUniqueOrThrow({ where: { workId } });
112
+ if (work.ownerId !== input.ownerId || work.participant !== participant)
113
+ fail('RESOURCE_WORK_CONFLICT', 'Work owner differs');
114
+ if (work.state === 'released')
115
+ return workFields(work);
116
+ const released = work.state === 'reserved' && input.disposition === 'not-started';
117
+ const updated = await tx.resourceParticipantWork.update({ where: { workId }, data: { state: released ? 'released' : 'unknown',
118
+ settledAt: released ? new Date() : null, settlement: released ? 'not-dispatched' : 'descendants-unverified', evidenceSha256: input.evidenceSha256 } });
119
+ return workFields(updated);
120
+ });
121
+ }
122
+ return { reserve, start, finish };
123
+ }
124
+ export const dashboardParticipantWork = participantWork('dashboard');
125
+ export const apiParticipantWork = participantWork('api');
126
+ /** Settle only the exact durable owner from a node-authenticated, independently
127
+ * supervised terminal receipt. This path observes; it never dispatches/retries. */
128
+ export async function settleParticipantWorkFromNodeReceipt(raw) {
129
+ const value = nodeSessionReceiptSchema.parse(raw);
130
+ if (!verifyNodeSessionReceiptSignature(value, RESULT_SIGNING_SECRET, RESULT_SIGNING_PREVIOUS_SECRET))
131
+ throw new HttpError(403, 'RESOURCE_EVIDENCE_INVALID', 'Node session receipt signature is invalid');
132
+ const db = getDb();
133
+ const found = await db.resourceParticipantWork.findUnique({ where: { workId: value.workId } });
134
+ if (!found)
135
+ throw new HttpError(404, 'RESOURCE_WORK_NOT_FOUND', 'Participant work not found');
136
+ return db.$transaction(async (tx) => {
137
+ await lockResourceNode(tx, found.nodeId);
138
+ const work = await tx.resourceParticipantWork.findUniqueOrThrow({ where: { workId: value.workId } });
139
+ const exact = work.nodeId === value.nodeId && work.nodeFqdn === value.nodeFqdn
140
+ && work.participant === value.participant && work.ownerId === value.ownerId && work.kind === value.kind
141
+ && work.operationSha256 === value.operationSha256 && work.requestSha256 === value.requestSha256
142
+ && work.fence?.toString() === value.fence && work.deadline?.getTime() === value.deadline;
143
+ const recomputed = work.nodeFqdn && work.operationSha256 && work.fence !== null && work.deadline
144
+ ? participantWorkRequestDigest({ participant: work.participant, workId: work.workId,
145
+ nodeId: work.nodeId, nodeFqdn: work.nodeFqdn, ownerId: work.ownerId, kind: participantWorkKindSchema.parse(work.kind),
146
+ epoch: work.epoch, barrierId: work.barrierId, operationSha256: work.operationSha256,
147
+ fence: work.fence.toString(), deadline: work.deadline.getTime() }) : null;
148
+ if (!exact || recomputed !== work.requestSha256 || !work.nodeFqdn || !work.operationSha256 || !work.requestSha256 || work.fence === null || !work.deadline)
149
+ fail('RESOURCE_EVIDENCE_INVALID', 'Node receipt does not bind the exact participant authority');
150
+ const descriptor = COMMAND_COMPLETION_REGISTRY.descriptors.find(item => item.id === value.completionDescriptorId);
151
+ if (value.completionRegistrySha256 !== COMMAND_COMPLETION_REGISTRY_SHA256 || !descriptor
152
+ || !descriptor.participantAuthority.allowedKinds.some(kind => kind === value.kind)
153
+ || descriptor.participantAuthority.effectSemantics !== 'read-only-node-diagnostic'
154
+ || value.effectBoundary !== descriptor.participantAuthority.effectBoundary)
155
+ fail('RESOURCE_EVIDENCE_INVALID', 'Node receipt completion owner is not in the exact Control registry');
156
+ if (!work.startedAt || !['running', 'unknown', 'released'].includes(work.state))
157
+ fail('RESOURCE_WORK_CONFLICT', 'Only started participant work can have a node terminal receipt');
158
+ if (value.terminalAt > Date.now() + 300_000)
159
+ fail('RESOURCE_EVIDENCE_INVALID', 'Node receipt terminal time is implausibly in the future');
160
+ const serialized = canonicalNodeSessionReceipt(value);
161
+ const receiptSha256 = digest(serialized);
162
+ if (work.state === 'released') {
163
+ if (work.nodeSessionReceiptSha256 !== receiptSha256 || work.nodeSessionReceiptJson !== serialized)
164
+ fail('RESOURCE_WORK_CONFLICT', 'Participant work already has another terminal settlement');
165
+ return workFields(work);
166
+ }
167
+ const reused = await tx.resourceParticipantWork.findFirst({ where: { nodeSessionReceiptSha256: receiptSha256 }, select: { workId: true } });
168
+ if (reused && reused.workId !== work.workId)
169
+ fail('RESOURCE_WORK_CONFLICT', 'Node receipt is already bound to other work');
170
+ const updated = await tx.resourceParticipantWork.update({ where: { workId: work.workId }, data: {
171
+ state: 'released', settledAt: new Date(value.terminalAt), settlement: `signed-node-session:${value.disposition}`,
172
+ evidenceSha256: value.resultSha256, nodeSessionReceiptSha256: receiptSha256, nodeSessionReceiptJson: serialized,
173
+ } });
174
+ return workFields(updated);
175
+ });
176
+ }
177
+ export function reconciliationSecret() {
178
+ const secret = process.env['RESOURCE_PARTICIPANT_RECONCILIATION_SECRET'] ?? '';
179
+ const conflicting = ['COMMAND_RECONCILIATION_SECRET', 'RESULT_SIGNING_SECRET', 'NODE_MANAGER_SECRET', 'API_SECRET', 'JWT_SECRET',
180
+ 'RESOURCE_ENROLLMENT_DASHBOARD_SECRET', 'SECURITY_APPROVAL_BRIDGE_TOKEN', 'CREDENTIAL_ROTATION_KEY', 'AGENT_DISCOVERY_SECRET'];
181
+ if (!/^[A-Za-z0-9._~+/=-]{32,4096}$/.test(secret)
182
+ || conflicting.some(name => process.env[name] && process.env[name] === secret))
183
+ throw new HttpError(503, 'RESOURCE_RECONCILIATION_UNAVAILABLE', 'Dedicated reconciliation authority is unavailable');
184
+ return secret;
185
+ }
186
+ function verifyReconciliationSignature(value) {
187
+ const secret = reconciliationSecret();
188
+ const expected = createHmac('sha256', secret).update(participantWorkReconciliationSigningPayload(value)).digest();
189
+ if (!timingSafeEqual(expected, Buffer.from(value.signature, 'hex')))
190
+ throw new HttpError(403, 'RESOURCE_EVIDENCE_INVALID', 'Participant reconciliation signature is invalid');
191
+ }
192
+ /**
193
+ * Release an unresolved participant owner only after an exact reviewed assertion.
194
+ * The assertion attests to independently verified quiescence; this function does
195
+ * not infer it from SSH exit, process age, transport status, or caller prose.
196
+ */
197
+ export async function reconcileParticipantWork(raw) {
198
+ const value = participantWorkReconciliationSchema.parse(raw);
199
+ verifyReconciliationSignature(value);
200
+ const db = getDb();
201
+ const found = await db.resourceParticipantWork.findUnique({ where: { workId: value.workId } });
202
+ if (!found)
203
+ throw new HttpError(404, 'RESOURCE_WORK_NOT_FOUND', 'Participant work not found');
204
+ return db.$transaction(async (tx) => {
205
+ await lockResourceNode(tx, found.nodeId);
206
+ await tx.$executeRaw `SELECT pg_advisory_xact_lock(hashtextextended(${`resource-participant-reconciliation:${value.decisionId}`}, 0))`;
207
+ const work = await tx.resourceParticipantWork.findUniqueOrThrow({ where: { workId: value.workId } });
208
+ const exact = work.nodeId === value.nodeId && work.nodeFqdn === value.nodeFqdn
209
+ && work.participant === value.participant && work.ownerId === value.ownerId && work.kind === value.kind
210
+ && work.epoch === value.epoch && work.barrierId === value.barrierId
211
+ && work.operationSha256 === value.operationSha256 && work.fence?.toString() === value.fence
212
+ && work.deadline?.getTime() === value.deadline && work.requestSha256 === value.requestSha256;
213
+ const recomputed = work.nodeFqdn && work.operationSha256 && work.fence !== null && work.deadline
214
+ ? participantWorkRequestDigest({ participant: work.participant, workId: work.workId,
215
+ nodeId: work.nodeId, nodeFqdn: work.nodeFqdn, ownerId: work.ownerId, kind: participantWorkKindSchema.parse(work.kind),
216
+ epoch: work.epoch, barrierId: work.barrierId, operationSha256: work.operationSha256,
217
+ fence: work.fence.toString(), deadline: work.deadline.getTime() }) : null;
218
+ if (!exact || recomputed !== work.requestSha256 || !work.nodeFqdn || !work.requestSha256)
219
+ fail('RESOURCE_EVIDENCE_INVALID', 'Reconciliation does not bind the exact participant request');
220
+ const serialized = JSON.stringify(value);
221
+ if (work.state === 'released') {
222
+ if (work.reconciliationDecisionId !== value.decisionId || work.reconciliationJson !== serialized)
223
+ fail('RESOURCE_WORK_CONFLICT', 'Participant work already has another terminal settlement');
224
+ return workFields(work);
225
+ }
226
+ if (!['running', 'unknown'].includes(work.state))
227
+ fail('RESOURCE_WORK_CONFLICT', 'Only started unresolved participant work can be reconciled');
228
+ if (value.expiresAt <= Date.now() || value.expiresAt > Date.now() + 300_000)
229
+ fail('RESOURCE_EVIDENCE_INVALID', 'Participant reconciliation validity is stale or too broad');
230
+ const reused = await tx.resourceParticipantWork.findFirst({ where: { reconciliationDecisionId: value.decisionId }, select: { workId: true } });
231
+ if (reused && reused.workId !== work.workId)
232
+ fail('RESOURCE_WORK_CONFLICT', 'Reconciliation decision is already bound to other work');
233
+ const updated = await tx.resourceParticipantWork.update({ where: { workId: work.workId }, data: {
234
+ state: 'released', settledAt: new Date(), settlement: `signed-reconciliation:${value.disposition}`,
235
+ evidenceSha256: value.evidenceSha256, reconciliationDecisionId: value.decisionId, reconciliationJson: serialized,
236
+ } });
237
+ return workFields(updated);
238
+ });
239
+ }
240
+ function historicalRecord(row) {
241
+ return { workId: row.workId, nodeId: row.nodeId, participant: row.participant, ownerId: row.ownerId,
242
+ kind: row.kind, epoch: row.epoch, barrierId: row.barrierId, nodeFqdn: row.nodeFqdn,
243
+ requestSha256: row.requestSha256, operationSha256: row.operationSha256, fence: row.fence?.toString() ?? null,
244
+ deadline: row.deadline?.toISOString() ?? null, state: row.state, createdAt: row.createdAt.toISOString(),
245
+ startedAt: row.startedAt?.toISOString() ?? null, settledAt: row.settledAt?.toISOString() ?? null,
246
+ settlement: row.settlement, evidenceSha256: row.evidenceSha256,
247
+ reconciliationDecisionId: row.reconciliationDecisionId, reconciliationJson: row.reconciliationJson,
248
+ nodeSessionReceiptSha256: row.nodeSessionReceiptSha256, nodeSessionReceiptJson: row.nodeSessionReceiptJson };
249
+ }
250
+ export async function inspectHistoricalParticipantWork(workId) {
251
+ const row = await getDb().resourceParticipantWork.findUnique({ where: { workId } });
252
+ if (!row)
253
+ throw new HttpError(404, 'RESOURCE_WORK_NOT_FOUND', 'Participant work not found');
254
+ const record = historicalRecord(row);
255
+ return { record, recordSha256: digest(record) };
256
+ }
257
+ /** Explicitly quarantine a preinstrumentation owner after an independently
258
+ * reviewed quiescence packet. The original outcome stays unknown in the signed
259
+ * assertion and in the settlement label. This cannot be used for v2 work. */
260
+ export async function reconcileHistoricalParticipantWork(raw) {
261
+ const value = historicalParticipantWorkReconciliationSchema.parse(raw);
262
+ const secret = reconciliationSecret();
263
+ const { signature, ...unsigned } = value;
264
+ const expected = createHmac('sha256', secret).update(historicalParticipantWorkSigningPayload(unsigned)).digest();
265
+ if (!timingSafeEqual(expected, Buffer.from(signature, 'hex')))
266
+ throw new HttpError(403, 'RESOURCE_EVIDENCE_INVALID', 'Historical reconciliation signature is invalid');
267
+ const found = await getDb().resourceParticipantWork.findUnique({ where: { workId: value.workId } });
268
+ if (!found)
269
+ throw new HttpError(404, 'RESOURCE_WORK_NOT_FOUND', 'Participant work not found');
270
+ return getDb().$transaction(async (tx) => {
271
+ await lockResourceNode(tx, found.nodeId);
272
+ await tx.$executeRaw `SELECT pg_advisory_xact_lock(hashtextextended(${`resource-participant-reconciliation:${value.decisionId}`}, 0))`;
273
+ const row = await tx.resourceParticipantWork.findUniqueOrThrow({ where: { workId: value.workId } });
274
+ const serialized = JSON.stringify(value);
275
+ if (row.state === 'released') {
276
+ if (row.reconciliationDecisionId !== value.decisionId || row.reconciliationJson !== serialized)
277
+ fail('RESOURCE_WORK_CONFLICT', 'Participant work already has another terminal settlement');
278
+ return workFields(row);
279
+ }
280
+ const hashes = [value.dashboardObservationSha256, value.nodeObservationSha256, value.operationAuditSha256];
281
+ if (row.nodeId !== value.nodeId || row.participant !== 'dashboard' || row.kind !== 'hermes-http'
282
+ || row.state !== 'unknown' || !row.startedAt || row.settledAt || row.settlement !== 'descendants-unverified'
283
+ || row.nodeFqdn !== null || row.operationSha256 !== null || row.requestSha256 !== null
284
+ || row.fence !== null || row.deadline !== null || row.reconciliationDecisionId !== null
285
+ || row.nodeSessionReceiptSha256 !== null || row.nodeSessionReceiptJson !== null
286
+ || digest(historicalRecord(row)) !== value.recordSha256
287
+ || new Set(hashes).size !== hashes.length
288
+ || value.observedAt < row.startedAt.getTime() || value.observedAt > Date.now() + 60_000
289
+ || value.observedAt < Date.now() - 300_000 || value.expiresAt <= Date.now()
290
+ || value.expiresAt > Date.now() + 300_000)
291
+ fail('RESOURCE_EVIDENCE_INVALID', 'Historical work or fresh quiescence evidence does not match');
292
+ const reused = await tx.resourceParticipantWork.findFirst({ where: { reconciliationDecisionId: value.decisionId }, select: { workId: true } });
293
+ if (reused && reused.workId !== row.workId)
294
+ fail('RESOURCE_WORK_CONFLICT', 'Reconciliation decision is already bound to other work');
295
+ const updated = await tx.resourceParticipantWork.update({ where: { workId: row.workId }, data: {
296
+ state: 'released', settledAt: new Date(), settlement: 'signed-historical-quiescence:outcome-unknown',
297
+ evidenceSha256: digest(hashes), reconciliationDecisionId: value.decisionId, reconciliationJson: serialized,
298
+ } });
299
+ return workFields(updated);
300
+ });
301
+ }
302
+ //# sourceMappingURL=resource-participant-work.js.map