@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,19 @@
1
+ import { getConnInfo } from '@hono/node-server/conninfo';
2
+ import { normalizeIpAddress } from './ip.js';
3
+ /**
4
+ * Read the peer address from the Node adapter before considering proxy
5
+ * metadata. Port 4000 accepts agents directly, so a caller-controlled
6
+ * x-forwarded-for header must not override the authenticated socket peer.
7
+ * The header remains a fallback for adapter-free tests and explicitly
8
+ * forwarded deployments where no socket binding is available.
9
+ */
10
+ export function heartbeatPeerAddress(c) {
11
+ let socketAddress = '';
12
+ try {
13
+ socketAddress = getConnInfo(c).remote.address ?? '';
14
+ }
15
+ catch { /* adapter-free request */ }
16
+ const forwardedAddress = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ?? '';
17
+ return normalizeIpAddress(socketAddress || forwardedAddress);
18
+ }
19
+ //# sourceMappingURL=heartbeat-peer.js.map
@@ -0,0 +1,45 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { getRedis } from '../queue/redis.js';
3
+ import { routePath } from 'hono/route';
4
+ import { HttpError } from '../middleware/error-handler.js';
5
+ const IDEMPOTENCY_TTL_SECONDS = 86_400;
6
+ const RELEASE_IF_OWNED = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
7
+ /** Keys are namespaced by operation scope and actor, so the same client key
8
+ * used on another route or by another user is not a duplicate. */
9
+ function idempotencyRedisKey(scope, actorId, key) {
10
+ return `idem:${createHash('sha256').update(JSON.stringify([scope, actorId, key])).digest('hex')}`;
11
+ }
12
+ /**
13
+ * Atomically claim an idempotency key (Redis SET NX, 24-hour TTL). Throws 409
14
+ * DUPLICATE_REQUEST if the key is already held for this scope and actor.
15
+ * Callers claim after validation, dry-run, and preflight checks, immediately
16
+ * before the first durable effect.
17
+ */
18
+ export async function claimIdempotency(scope, actorId, key) {
19
+ const redis = getRedis();
20
+ const redisKey = idempotencyRedisKey(scope, actorId, key);
21
+ const token = randomUUID();
22
+ const result = await redis.set(redisKey, token, 'EX', IDEMPOTENCY_TTL_SECONDS, 'NX');
23
+ if (result === null) {
24
+ throw new HttpError(409, 'DUPLICATE_REQUEST', `Idempotency key already used: ${key}`);
25
+ }
26
+ return {
27
+ async release() {
28
+ await redis.eval(RELEASE_IF_OWNED, 1, redisKey, token).catch(() => undefined);
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * Enforce idempotency when the `x-idempotency-key` header is present.
34
+ * For endpoints where the key is optional — callers that supply it get
35
+ * duplicate-request protection; callers that omit it proceed normally.
36
+ * The key is scoped to the matched route and the authenticated actor.
37
+ */
38
+ export async function enforceIdempotencyFromHeader(c) {
39
+ const key = c.req.header('x-idempotency-key');
40
+ if (!key)
41
+ return undefined;
42
+ const actorId = c.var.user?.userId ?? 'anonymous';
43
+ return claimIdempotency(`${c.req.method} ${routePath(c) || c.req.path}`, actorId, key);
44
+ }
45
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1,25 @@
1
+ import { isIP } from 'node:net';
2
+ /** Strip IPv6-mapped IPv4 prefix and normalize empty values. */
3
+ export function normalizeIpAddress(rawIp) {
4
+ if (!rawIp)
5
+ return '';
6
+ const trimmed = rawIp.trim();
7
+ return trimmed.startsWith('::ffff:') ? trimmed.slice(7) : trimmed;
8
+ }
9
+ /** Check if an IP is loopback, unspecified, or otherwise unroutable. */
10
+ export function isLoopbackOrUnroutableIp(ipAddress) {
11
+ const ip = normalizeIpAddress(ipAddress);
12
+ if (!ip)
13
+ return true;
14
+ if (ip === '::1' || ip === '::' || ip === '0.0.0.0')
15
+ return true;
16
+ if (ip.startsWith('127.'))
17
+ return true;
18
+ return false;
19
+ }
20
+ /** Check if an IP is a valid, routable node address. */
21
+ export function isUsableNodeIp(ipAddress) {
22
+ const ip = normalizeIpAddress(ipAddress);
23
+ return isIP(ip) !== 0 && !isLoopbackOrUnroutableIp(ip);
24
+ }
25
+ //# sourceMappingURL=ip.js.map
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Typed parse/serialize helpers for JSON-encoded Prisma string fields.
3
+ *
4
+ * Magnus deliberately stores these wire-compatible arrays and objects as
5
+ * serialized JSON strings. These helpers centralize the serde logic to avoid scattered
6
+ * `JSON.stringify` / `JSON.parse` casts throughout route files.
7
+ */
8
+ /**
9
+ * Parse a JSON-encoded string array from a Prisma field.
10
+ * Falls back to an empty array if the value is null, undefined, or invalid JSON.
11
+ */
12
+ export function parseStringArray(value) {
13
+ if (!value)
14
+ return [];
15
+ if (Array.isArray(value))
16
+ return value;
17
+ try {
18
+ const parsed = JSON.parse(value);
19
+ return Array.isArray(parsed) ? parsed : [];
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ /**
26
+ * Serialize a string array to a JSON string for storage in a Prisma field.
27
+ * Accepts undefined for convenience (returns '[]').
28
+ */
29
+ export function serializeStringArray(value) {
30
+ return JSON.stringify(value ?? []);
31
+ }
32
+ /**
33
+ * Parse a JSON-encoded generic array from a Prisma field.
34
+ * Falls back to the provided default if null, undefined, or invalid JSON.
35
+ */
36
+ export function parseJsonArray(value, fallback = []) {
37
+ if (!value)
38
+ return fallback;
39
+ if (Array.isArray(value))
40
+ return value;
41
+ try {
42
+ const parsed = JSON.parse(value);
43
+ return Array.isArray(parsed) ? parsed : fallback;
44
+ }
45
+ catch {
46
+ return fallback;
47
+ }
48
+ }
49
+ /**
50
+ * Parse a JSON-encoded `Record<string, string>` from a Prisma field.
51
+ * Falls back to an empty object if null, undefined, or invalid JSON.
52
+ */
53
+ export function parseStringRecord(value) {
54
+ if (!value)
55
+ return {};
56
+ if (typeof value === 'object' && !Array.isArray(value))
57
+ return value;
58
+ try {
59
+ const parsed = JSON.parse(value);
60
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
61
+ ? parsed
62
+ : {};
63
+ }
64
+ catch {
65
+ return {};
66
+ }
67
+ }
68
+ /**
69
+ * Parse a JSON-encoded `Record<string, unknown>` from a Prisma field.
70
+ * Falls back to an empty object if null, undefined, or invalid JSON.
71
+ */
72
+ export function parseJsonRecord(value) {
73
+ if (!value)
74
+ return {};
75
+ if (typeof value === 'object' && !Array.isArray(value))
76
+ return value;
77
+ try {
78
+ const parsed = JSON.parse(value);
79
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
80
+ ? parsed
81
+ : {};
82
+ }
83
+ catch {
84
+ return {};
85
+ }
86
+ }
87
+ /**
88
+ * Serialize a record to a JSON string for storage in a Prisma field.
89
+ * Accepts undefined for convenience (returns '{}').
90
+ */
91
+ export function serializeRecord(value) {
92
+ return JSON.stringify(value ?? {});
93
+ }
94
+ //# sourceMappingURL=json-fields.js.map
@@ -0,0 +1,39 @@
1
+ /** Verified TLS options for loopback connections to local Magnus services. */
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { isIP } from 'node:net';
4
+ export function verifiedLocalTlsOptions(caPath, servername) {
5
+ const options = {
6
+ rejectUnauthorized: true,
7
+ };
8
+ const normalizedCaPath = caPath?.trim();
9
+ if (normalizedCaPath) {
10
+ if (!existsSync(normalizedCaPath)) {
11
+ throw new Error(`Configured TLS CA does not exist: ${normalizedCaPath}`);
12
+ }
13
+ options.ca = readFileSync(normalizedCaPath);
14
+ }
15
+ const normalizedServername = servername?.trim();
16
+ if (normalizedServername && isIP(normalizedServername) === 0) {
17
+ options.servername = normalizedServername;
18
+ }
19
+ return options;
20
+ }
21
+ function controlApiHostname() {
22
+ try {
23
+ return new URL(process.env['CONTROL_API_URL'] ?? '').hostname || undefined;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ export function controlHostAgentTlsServername() {
30
+ const configured = (process.env['CONTROL_HOST_AGENT_TLS_SERVERNAME'] ?? controlApiHostname())?.trim().toLowerCase();
31
+ return configured && isIP(configured) === 0 ? configured : undefined;
32
+ }
33
+ export function controlApiLoopbackTlsOptions() {
34
+ return verifiedLocalTlsOptions(process.env['CONTROL_API_CA_PATH'], process.env['CONTROL_API_TLS_SERVERNAME'] ?? controlApiHostname());
35
+ }
36
+ export function controlHostAgentTlsOptions() {
37
+ return verifiedLocalTlsOptions(process.env['CONTROL_HOST_AGENT_CA_PATH'] ?? process.env['CONTROL_API_CA_PATH'], controlHostAgentTlsServername());
38
+ }
39
+ //# sourceMappingURL=local-tls.js.map
@@ -0,0 +1,15 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ /**
4
+ * Base directory for server-owned local data (backups, audit logs, state).
5
+ * Defaults to `~/.magnus`; override with `MAGNUS_HOME`.
6
+ */
7
+ export function magnusHome(env = process.env) {
8
+ const configured = env['MAGNUS_HOME']?.trim();
9
+ return configured ? resolve(configured) : join(homedir(), '.magnus');
10
+ }
11
+ /** Resolve a path below {@link magnusHome}. */
12
+ export function magnusHomePath(...segments) {
13
+ return join(magnusHome(), ...segments);
14
+ }
15
+ //# sourceMappingURL=magnus-home.js.map
@@ -0,0 +1,59 @@
1
+ import { normalizeIpAddress } from './ip.js';
2
+ import { isIP } from 'node:net';
3
+ /**
4
+ * Build the base URL for a node-manager agent.
5
+ *
6
+ * When TLS_CERT_PATH and TLS_KEY_PATH are configured, agents are assumed to
7
+ * run behind HTTPS. Otherwise falls back to plain HTTP.
8
+ */
9
+ export function getNodeBaseUrl(ipAddress, port) {
10
+ const useTls = process.env['TLS_CERT_PATH'] && process.env['TLS_KEY_PATH'];
11
+ const protocol = useTls ? 'https' : 'http';
12
+ return `${protocol}://${ipAddress}:${port}`;
13
+ }
14
+ const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
15
+ /**
16
+ * Derive the certificate hostname an agent should advertise from its reported
17
+ * OS hostname and the operator-configured TLS suffix. The heartbeat handler
18
+ * still resolves this name and requires the resolved address to appear in the
19
+ * authenticated agent's network/caller evidence before persisting it.
20
+ */
21
+ export function deriveAgentCertificateHostname(reportedFqdn, configuredSuffix) {
22
+ const reported = reportedFqdn?.trim().toLowerCase().replace(/\.$/, '') ?? '';
23
+ const suffix = configuredSuffix
24
+ ?.trim()
25
+ .toLowerCase()
26
+ .replace(/^\.+|\.+$/g, '') ?? '';
27
+ if (!reported || !suffix || !suffix.includes('.'))
28
+ return null;
29
+ const suffixLabels = suffix.split('.');
30
+ if (!suffixLabels.every((label) => DNS_LABEL.test(label)))
31
+ return null;
32
+ if (reported.endsWith(`.${suffix}`))
33
+ return reported;
34
+ const shortName = reported.split('.')[0] ?? '';
35
+ if (!DNS_LABEL.test(shortName))
36
+ return null;
37
+ const hostname = `${shortName}.${suffix}`;
38
+ return hostname.length <= 253 ? hostname : null;
39
+ }
40
+ /** Accept the derived certificate name only when DNS points back to this heartbeat. */
41
+ export function verifyAgentCertificateHostname(certificateHostname, resolvedAddress, heartbeatAddresses) {
42
+ if (!certificateHostname || !resolvedAddress)
43
+ return null;
44
+ const resolved = normalizeIpAddress(resolvedAddress);
45
+ const observed = new Set(heartbeatAddresses.map((value) => normalizeIpAddress(value)));
46
+ return resolved && observed.has(resolved) ? certificateHostname : null;
47
+ }
48
+ /** Keep inventory hostname aligned with the endpoint identity actually verified by TLS. */
49
+ export function selectAgentEndpointHostname(verifiedCertificateHostname, reportedFqdn, existingHostname, existingEndpoint) {
50
+ const verified = verifiedCertificateHostname?.trim().toLowerCase();
51
+ if (verified)
52
+ return verified;
53
+ const existing = existingHostname?.trim().toLowerCase();
54
+ const endpoint = existingEndpoint?.trim().toLowerCase();
55
+ if (existing && endpoint && existing === endpoint && isIP(existing) === 0)
56
+ return existing;
57
+ return reportedFqdn.trim().toLowerCase();
58
+ }
59
+ //# sourceMappingURL=node-url.js.map
@@ -0,0 +1,26 @@
1
+ const SECRET_PATTERNS = [
2
+ /sk-[A-Za-z0-9_-]{12,}/g,
3
+ /(?:api[_-]?key|token|secret|password|passwd|private[_-]?key)\s*[:=]\s*["']?[^"',\s}]+/gi,
4
+ /Bearer\s+[A-Za-z0-9._~+/-]+=*/g,
5
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
6
+ ];
7
+ export function redactSecretText(value) {
8
+ return SECRET_PATTERNS.reduce((text, pattern) => text.replace(pattern, '[REDACTED]'), value);
9
+ }
10
+ export function redactUnknown(value) {
11
+ if (typeof value === 'string')
12
+ return redactSecretText(value);
13
+ if (Array.isArray(value))
14
+ return value.map((item) => redactUnknown(item));
15
+ if (value && typeof value === 'object') {
16
+ const entries = Object.entries(value).map(([key, item]) => {
17
+ if (/api[_-]?key|token|secret|password|passwd|private[_-]?key/i.test(key)) {
18
+ return [key, '[REDACTED]'];
19
+ }
20
+ return [key, redactUnknown(item)];
21
+ });
22
+ return Object.fromEntries(entries);
23
+ }
24
+ return value;
25
+ }
26
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1,13 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ /** Constant-time string comparison to prevent timing attacks. */
3
+ export function safeEqual(a, b) {
4
+ const bufA = Buffer.from(a);
5
+ const bufB = Buffer.from(b);
6
+ if (bufA.length !== bufB.length) {
7
+ // Compare against self to avoid leaking length information via timing
8
+ timingSafeEqual(bufA, bufA);
9
+ return false;
10
+ }
11
+ return timingSafeEqual(bufA, bufB);
12
+ }
13
+ //# sourceMappingURL=safe-equal.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Well-known sentinel node IDs.
3
+ *
4
+ * These virtual Node records satisfy the `LogEntry.nodeId` foreign-key
5
+ * constraint for internal log sources (Control API process logs, Dashboard
6
+ * error reports). They are seeded by `prisma/seed.ts` with
7
+ * `metadata: '{"sentinel":true}'` and must be excluded from all fleet-facing
8
+ * queries (node listing, scan, heartbeat monitor, etc.).
9
+ */
10
+ /** Sentinel node representing the Control API process itself. */
11
+ export const CONTROL_API_NODE_ID = '__control-api__';
12
+ /** Sentinel node representing the Next.js Dashboard. */
13
+ export const DASHBOARD_NODE_ID = '__dashboard__';
14
+ /** Array of all sentinel node IDs — use in Prisma `id: { notIn: … }` filters. */
15
+ export const SENTINEL_NODE_IDS = [CONTROL_API_NODE_ID, DASHBOARD_NODE_ID];
16
+ //# sourceMappingURL=sentinel-nodes.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Request body / query parsing with Zod schemas.
3
+ *
4
+ * Usage:
5
+ * const body = await parseBody(c, mySchema);
6
+ * // body is fully typed & validated
7
+ */
8
+ import { HttpError } from '../middleware/error-handler.js';
9
+ /** Format Zod issues into a human-readable string. */
10
+ function formatZodError(error) {
11
+ return error.issues
12
+ .map((i) => {
13
+ const path = i.path.length ? `${i.path.join('.')}: ` : '';
14
+ return `${path}${i.message}`;
15
+ })
16
+ .join('; ');
17
+ }
18
+ /**
19
+ * Parse and validate the JSON request body against a Zod schema.
20
+ * Throws HttpError 400 with a machine-readable VALIDATION_ERROR code.
21
+ */
22
+ export async function parseBody(c, schema) {
23
+ const raw = await c.req.json();
24
+ const result = schema.safeParse(raw);
25
+ if (!result.success) {
26
+ throw new HttpError(400, 'VALIDATION_ERROR', formatZodError(result.error));
27
+ }
28
+ return result.data;
29
+ }
30
+ /**
31
+ * Parse and validate an arbitrary value (e.g. upstream response) against a Zod schema.
32
+ * Returns `{ success: true, data }` or `{ success: false, error }`.
33
+ */
34
+ export function safeParse(value, schema) {
35
+ const result = schema.safeParse(value);
36
+ if (!result.success) {
37
+ return { success: false, error: formatZodError(result.error) };
38
+ }
39
+ return { success: true, data: result.data };
40
+ }
41
+ //# sourceMappingURL=validate.js.map
package/package.json CHANGED
@@ -1,6 +1,121 @@
1
1
  {
2
2
  "name": "@hivemagnus/magnus-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.2",
4
+ "description": "Magnus server: the fleet control plane API (Hono, PostgreSQL, Redis) for nodes running magnus-agent, with optional Hive IAM, Hive Router, HiveMem, Hermes Agent and external security console integrations.",
5
+ "keywords": [
6
+ "magnus",
7
+ "fleet",
8
+ "control-plane",
9
+ "server",
10
+ "api",
11
+ "hono",
12
+ "postgresql",
13
+ "redis",
14
+ "ops",
15
+ "hermes-agent"
16
+ ],
17
+ "license": "Apache-2.0",
18
+ "author": "ChefTaco",
19
+ "homepage": "https://github.com/ChefTaco/magnus/tree/main/services/control-api#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/ChefTaco/magnus/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ChefTaco/magnus.git",
26
+ "directory": "services/control-api"
27
+ },
28
+ "type": "module",
29
+ "bin": {
30
+ "magnus-server": "dist/cli.js"
31
+ },
32
+ "main": "dist/index.js",
33
+ "files": [
34
+ "dist/**/*.js",
35
+ "dist/ai/*.md",
36
+ "!dist/**/*.test.*",
37
+ "!dist/**/*.map",
38
+ "!dist/**/*-test-fixture.js",
39
+ "prisma/schema.prisma",
40
+ "prisma/migrations",
41
+ "prisma.config.ts",
42
+ "agent-version.json",
43
+ "README.md",
44
+ "LICENSE",
45
+ ".env.example"
46
+ ],
47
+ "engines": {
48
+ "node": "^24.22.1 || ^26.13.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "overrides": {
54
+ "@prisma/config": {
55
+ "deepmerge-ts": "8.0.1"
56
+ },
57
+ "@prisma/dev": {
58
+ "@hono/node-server": "2.1.0"
59
+ },
60
+ "fast-uri": "3.1.9",
61
+ "mysql2": "3.24.4"
62
+ },
63
+ "scripts": {
64
+ "build": "npm run clean && tsc --build && node scripts/copy-assets.mjs",
65
+ "dev": "tsx watch src/index.ts",
66
+ "start": "node dist/cli.js start",
67
+ "clean": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\"",
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "vitest run --passWithNoTests",
70
+ "test:integration": "vitest run --config vitest.integration.config.ts --passWithNoTests --fileParallelism=false",
71
+ "test:ci": "vitest run --coverage",
72
+ "lint": "eslint src/",
73
+ "db:generate": "prisma generate",
74
+ "db:migrate": "prisma migrate dev",
75
+ "db:migrate:deploy": "prisma migrate deploy",
76
+ "webhooks:migrate-credentials": "tsx scripts/migrate-webhook-credentials.ts",
77
+ "db:seed": "tsx prisma/seed.ts",
78
+ "db:studio": "prisma studio",
79
+ "db:reset": "prisma migrate reset --force",
80
+ "engineering:recovery-exercise": "node dist/scripts/run-disposable-recovery-exercise.js",
81
+ "engineering:recovery-reap": "node dist/scripts/run-disposable-recovery-exercise.js --reap-orphans",
82
+ "check": "node dist/cli.js check",
83
+ "compose:check": "bash scripts/check-compose.sh",
84
+ "prepublishOnly": "npm run db:generate && npm run clean && npm run build"
85
+ },
86
+ "dependencies": {
87
+ "@hivemagnus/magnus-contracts": "1.0.2",
88
+ "@hono/node-server": "^2.1.5",
89
+ "@hono/swagger-ui": "^0.6.2",
90
+ "@prisma/adapter-pg": "7.10.1",
91
+ "@prisma/client": "^7.10.1",
92
+ "argon2": "^0.45.1",
93
+ "bullmq": "^5.30.0",
94
+ "dotenv": "^18.0.9",
95
+ "hono": "4.13.14",
96
+ "ioredis": "^5.4.0",
97
+ "jose": "^6.2.13",
98
+ "ky": "^2.2.0",
99
+ "nanoid": "^5.0.0",
100
+ "pg": "^8.23.1",
101
+ "ssh2": "^1.17.0",
102
+ "ws": "^8.22.0",
103
+ "yaml": "2.9.1",
104
+ "zod": "^4.6.7",
105
+ "@hivemagnus/magnus-skills": "1.0.2",
106
+ "prisma": "^7.10.1"
107
+ },
108
+ "devDependencies": {
109
+ "@types/node": "^22.20.5",
110
+ "@types/pg": "^8.23.1",
111
+ "@types/ssh2": "^1.15.6",
112
+ "@types/ws": "^8.18.2",
113
+ "tsx": "^4.23.15",
114
+ "typescript": "^5.8.3",
115
+ "vitest": "^4.1.12"
116
+ },
117
+ "prisma": {
118
+ "schema": "prisma/schema.prisma",
119
+ "seed": "tsx prisma/seed.ts"
120
+ }
121
+ }