@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.
- package/.env.example +171 -0
- package/LICENSE +202 -0
- package/README.md +169 -2
- package/agent-version.json +1 -0
- package/dist/ai/HEARTBEAT.md +56 -0
- package/dist/ai/IDENTITY.md +54 -0
- package/dist/ai/MEMORY.md +40 -0
- package/dist/ai/TOOLS.md +538 -0
- package/dist/ai/USER.md +32 -0
- package/dist/cli/args.js +62 -0
- package/dist/cli/env-catalog.js +131 -0
- package/dist/cli/preflight.js +89 -0
- package/dist/cli.js +175 -0
- package/dist/config/cors.js +16 -0
- package/dist/config/env.js +392 -0
- package/dist/config/features.js +16 -0
- package/dist/config/index.js +3 -0
- package/dist/config/listener-security.js +38 -0
- package/dist/config/version.js +31 -0
- package/dist/db/client.js +44 -0
- package/dist/db/generated/prisma/browser.js +18 -0
- package/dist/db/generated/prisma/client.js +35 -0
- package/dist/db/generated/prisma/commonInputTypes.js +11 -0
- package/dist/db/generated/prisma/enums.js +42 -0
- package/dist/db/generated/prisma/internal/class.js +50 -0
- package/dist/db/generated/prisma/internal/prismaNamespace.js +1598 -0
- package/dist/db/generated/prisma/internal/prismaNamespaceBrowser.js +1569 -0
- package/dist/db/generated/prisma/models/A2ATask.js +2 -0
- package/dist/db/generated/prisma/models/AdmissionFence.js +2 -0
- package/dist/db/generated/prisma/models/Agent.js +2 -0
- package/dist/db/generated/prisma/models/AiConversation.js +2 -0
- package/dist/db/generated/prisma/models/AiDashboard.js +2 -0
- package/dist/db/generated/prisma/models/AiMemory.js +2 -0
- package/dist/db/generated/prisma/models/AiMessage.js +2 -0
- package/dist/db/generated/prisma/models/AiScheduledOp.js +2 -0
- package/dist/db/generated/prisma/models/AlertConditionState.js +2 -0
- package/dist/db/generated/prisma/models/AlertRule.js +2 -0
- package/dist/db/generated/prisma/models/AlertViolation.js +2 -0
- package/dist/db/generated/prisma/models/AnomalyEvent.js +2 -0
- package/dist/db/generated/prisma/models/ApplicationObservation.js +2 -0
- package/dist/db/generated/prisma/models/ArdentInstallation.js +2 -0
- package/dist/db/generated/prisma/models/ArdentProfile.js +2 -0
- package/dist/db/generated/prisma/models/ArdentScopeEnrollment.js +2 -0
- package/dist/db/generated/prisma/models/CommandResourceFence.js +2 -0
- package/dist/db/generated/prisma/models/CommandResourceLease.js +2 -0
- package/dist/db/generated/prisma/models/CommandResult.js +2 -0
- package/dist/db/generated/prisma/models/CronJob.js +2 -0
- package/dist/db/generated/prisma/models/CronJobExecution.js +2 -0
- package/dist/db/generated/prisma/models/DeployJob.js +2 -0
- package/dist/db/generated/prisma/models/DriftScan.js +2 -0
- package/dist/db/generated/prisma/models/EngineeringMetricSample.js +2 -0
- package/dist/db/generated/prisma/models/EngineeringRecoveryExercise.js +2 -0
- package/dist/db/generated/prisma/models/ExecutionRun.js +2 -0
- package/dist/db/generated/prisma/models/FilePatchTask.js +2 -0
- package/dist/db/generated/prisma/models/GpuSnapshot.js +2 -0
- package/dist/db/generated/prisma/models/HealthSnapshot.js +2 -0
- package/dist/db/generated/prisma/models/HermesCoreRollout.js +2 -0
- package/dist/db/generated/prisma/models/HermesCoreRolloutHost.js +2 -0
- package/dist/db/generated/prisma/models/HermesCredentialPool.js +2 -0
- package/dist/db/generated/prisma/models/HermesCredentialPoolAttachment.js +2 -0
- package/dist/db/generated/prisma/models/HermesCronJob.js +2 -0
- package/dist/db/generated/prisma/models/HermesHook.js +2 -0
- package/dist/db/generated/prisma/models/HermesInstallation.js +2 -0
- package/dist/db/generated/prisma/models/HermesMcpServer.js +2 -0
- package/dist/db/generated/prisma/models/HermesMcpServerAttachment.js +2 -0
- package/dist/db/generated/prisma/models/HermesMessagingBinding.js +2 -0
- package/dist/db/generated/prisma/models/HermesOperationJob.js +2 -0
- package/dist/db/generated/prisma/models/HermesPluginInstall.js +2 -0
- package/dist/db/generated/prisma/models/HermesProfile.js +2 -0
- package/dist/db/generated/prisma/models/HermesProviderConfig.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryClient.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryInventory.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryReceipt.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryRevision.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryTarget.js +2 -0
- package/dist/db/generated/prisma/models/HermesRole.js +2 -0
- package/dist/db/generated/prisma/models/HermesRoleApplication.js +2 -0
- package/dist/db/generated/prisma/models/HermesSkillAssignment.js +2 -0
- package/dist/db/generated/prisma/models/HermesToolsetPolicy.js +2 -0
- package/dist/db/generated/prisma/models/IncidentLesson.js +2 -0
- package/dist/db/generated/prisma/models/LibraryCategory.js +2 -0
- package/dist/db/generated/prisma/models/LibraryFile.js +2 -0
- package/dist/db/generated/prisma/models/LogEntry.js +2 -0
- package/dist/db/generated/prisma/models/MagnusReleaseCanary.js +2 -0
- package/dist/db/generated/prisma/models/MaintenanceWindow.js +2 -0
- package/dist/db/generated/prisma/models/ManagedCronDefinition.js +2 -0
- package/dist/db/generated/prisma/models/ManagedCronOccurrence.js +2 -0
- package/dist/db/generated/prisma/models/Node.js +2 -0
- package/dist/db/generated/prisma/models/Notification.js +2 -0
- package/dist/db/generated/prisma/models/NotificationPreference.js +2 -0
- package/dist/db/generated/prisma/models/RemediationExecution.js +2 -0
- package/dist/db/generated/prisma/models/RemediationRule.js +2 -0
- package/dist/db/generated/prisma/models/RemoteCommand.js +2 -0
- package/dist/db/generated/prisma/models/ResourceDrainParticipant.js +2 -0
- package/dist/db/generated/prisma/models/ResourceEnrollment.js +2 -0
- package/dist/db/generated/prisma/models/ResourceParticipantWork.js +2 -0
- package/dist/db/generated/prisma/models/Role.js +2 -0
- package/dist/db/generated/prisma/models/RoleApplication.js +2 -0
- package/dist/db/generated/prisma/models/RolloutTarget.js +2 -0
- package/dist/db/generated/prisma/models/SecurityExportOutbox.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionAction.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionActionApproval.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCase.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseActivity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseEntity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseEvent.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseRelationship.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCollector.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCommandOutbox.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionDeletionReceipt.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionDelivery.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEntity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEntityAlias.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEvent.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEventEntity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEvidence.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionInvestigation.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionNonce.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionReceiptChainHead.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionRuleVersion.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionSourceState.js +2 -0
- package/dist/db/generated/prisma/models/SecurityServiceState.js +2 -0
- package/dist/db/generated/prisma/models/SharedResourceFence.js +2 -0
- package/dist/db/generated/prisma/models/SharedResourceLease.js +2 -0
- package/dist/db/generated/prisma/models/SharedResourceLeaseResource.js +2 -0
- package/dist/db/generated/prisma/models/SkillInstallation.js +2 -0
- package/dist/db/generated/prisma/models/SkillRegistryEntry.js +2 -0
- package/dist/db/generated/prisma/models/SkillTag.js +2 -0
- package/dist/db/generated/prisma/models/SkillVersion.js +2 -0
- package/dist/db/generated/prisma/models/TeamApprovalDecision.js +2 -0
- package/dist/db/generated/prisma/models/User.js +2 -0
- package/dist/db/generated/prisma/models/WebhookDelivery.js +2 -0
- package/dist/db/generated/prisma/models/WebhookEndpoint.js +2 -0
- package/dist/db/generated/prisma/models.js +2 -0
- package/dist/db/index.js +3 -0
- package/dist/db/read-retry.js +35 -0
- package/dist/db/seed.js +348 -0
- package/dist/index.js +162 -0
- package/dist/middleware/auth.js +361 -0
- package/dist/middleware/body-limit.js +14 -0
- package/dist/middleware/correlation-id.js +13 -0
- package/dist/middleware/error-handler.js +28 -0
- package/dist/middleware/iam-token.js +21 -0
- package/dist/middleware/rate-limit.js +78 -0
- package/dist/middleware/request-logger.js +85 -0
- package/dist/middleware/security-headers.js +23 -0
- package/dist/middleware/tracing.js +120 -0
- package/dist/middleware/user-status.js +33 -0
- package/dist/openapi/spec.js +2656 -0
- package/dist/queue/redis.js +20 -0
- package/dist/queue/requeue.js +27 -0
- package/dist/queue/workers.js +342 -0
- package/dist/routes/ai-schedule.js +73 -0
- package/dist/routes/alerts.js +79 -0
- package/dist/routes/anomalies.js +101 -0
- package/dist/routes/ardent.js +82 -0
- package/dist/routes/assistant.js +417 -0
- package/dist/routes/audit.js +138 -0
- package/dist/routes/codex.js +65 -0
- package/dist/routes/commands.js +226 -0
- package/dist/routes/credentials.js +58 -0
- package/dist/routes/cron.js +292 -0
- package/dist/routes/dependencies.js +49 -0
- package/dist/routes/deploy.js +18 -0
- package/dist/routes/discovery.js +168 -0
- package/dist/routes/docs.js +24 -0
- package/dist/routes/drift.js +126 -0
- package/dist/routes/events-stream.js +87 -0
- package/dist/routes/files.js +214 -0
- package/dist/routes/fleet-checks.js +21 -0
- package/dist/routes/health.js +133 -0
- package/dist/routes/heartbeat.js +545 -0
- package/dist/routes/hermes-registry.js +278 -0
- package/dist/routes/hermes.js +61 -0
- package/dist/routes/library.js +402 -0
- package/dist/routes/logs.js +172 -0
- package/dist/routes/managed-cron.js +23 -0
- package/dist/routes/mcp.js +39 -0
- package/dist/routes/metrics.js +187 -0
- package/dist/routes/nodes.js +951 -0
- package/dist/routes/notifications.js +130 -0
- package/dist/routes/ops.js +258 -0
- package/dist/routes/orchestra.js +430 -0
- package/dist/routes/predictions.js +23 -0
- package/dist/routes/profiling.js +68 -0
- package/dist/routes/quick-command.js +72 -0
- package/dist/routes/remediation.js +111 -0
- package/dist/routes/resource-leases.js +297 -0
- package/dist/routes/roles.js +370 -0
- package/dist/routes/rollout.js +124 -0
- package/dist/routes/scan.js +116 -0
- package/dist/routes/security-backup.js +58 -0
- package/dist/routes/security-fusion.js +605 -0
- package/dist/routes/security-integration.js +71 -0
- package/dist/routes/self-update.js +129 -0
- package/dist/routes/shared-resource-leases.js +78 -0
- package/dist/routes/skills.js +485 -0
- package/dist/routes/stats.js +96 -0
- package/dist/routes/team-approvals.js +23 -0
- package/dist/routes/terminal-ticket.js +15 -0
- package/dist/routes/terminal.js +17 -0
- package/dist/routes/topology.js +37 -0
- package/dist/routes/users.js +219 -0
- package/dist/routes/webhooks.js +137 -0
- package/dist/schemas/command-reconciliation.js +14 -0
- package/dist/schemas/commands.js +51 -0
- package/dist/schemas/cron.js +33 -0
- package/dist/schemas/deploy.js +20 -0
- package/dist/schemas/files.js +14 -0
- package/dist/schemas/hermes.js +69 -0
- package/dist/schemas/index.js +11 -0
- package/dist/schemas/library.js +48 -0
- package/dist/schemas/logs.js +35 -0
- package/dist/schemas/managed-cron.js +127 -0
- package/dist/schemas/node-responses.js +87 -0
- package/dist/schemas/node-session-receipt.js +84 -0
- package/dist/schemas/nodes.js +36 -0
- package/dist/schemas/orchestra.js +118 -0
- package/dist/schemas/resource-participant-reconciliation.js +99 -0
- package/dist/schemas/roles.js +54 -0
- package/dist/schemas/shared-resource-lease.js +132 -0
- package/dist/schemas/skills.js +44 -0
- package/dist/schemas/users.js +28 -0
- package/dist/scripts/run-disposable-recovery-exercise.js +39 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/node-api-client.js +474 -0
- package/dist/security/approval-assertion.js +66 -0
- package/dist/security/command-completion-registry.js +35 -0
- package/dist/security/command-request.js +12 -0
- package/dist/security/command-result-signature.js +34 -0
- package/dist/security/managed-http.js +45 -0
- package/dist/security/native-completion-registry.js +44 -0
- package/dist/server.js +178 -0
- package/dist/services/admission-fence.js +71 -0
- package/dist/services/ai-assistant-service.js +2523 -0
- package/dist/services/ai-correlation-service.js +225 -0
- package/dist/services/ai-export-service.js +141 -0
- package/dist/services/ai-insights-service.js +219 -0
- package/dist/services/ai-schedule-service.js +145 -0
- package/dist/services/ai-scratchpad-service.js +222 -0
- package/dist/services/ai-tools.js +1343 -0
- package/dist/services/alert-service.js +313 -0
- package/dist/services/anomaly-detector.js +419 -0
- package/dist/services/api-local-work.js +79 -0
- package/dist/services/application-probes.js +309 -0
- package/dist/services/ardent-scope-enrollment.js +123 -0
- package/dist/services/ardent-service.js +86 -0
- package/dist/services/audit-service.js +308 -0
- package/dist/services/codex-service.js +209 -0
- package/dist/services/command-lifecycle.js +60 -0
- package/dist/services/command-resource-leases.js +305 -0
- package/dist/services/control-host-node.js +242 -0
- package/dist/services/credential-rotation-service.js +294 -0
- package/dist/services/dependency-service.js +145 -0
- package/dist/services/disposable-recovery-runner.js +595 -0
- package/dist/services/docker-group-health.js +121 -0
- package/dist/services/engineering-assurance.js +492 -0
- package/dist/services/engineering-evidence-ledger.js +94 -0
- package/dist/services/engineering-metric-evidence.js +26 -0
- package/dist/services/event-bus.js +227 -0
- package/dist/services/fleet-check-log.js +102 -0
- package/dist/services/forecast-service.js +154 -0
- package/dist/services/health-retention.js +175 -0
- package/dist/services/heartbeat-stream.js +63 -0
- package/dist/services/hermes-catalog-import.js +62 -0
- package/dist/services/hermes-cli-parser.js +671 -0
- package/dist/services/hermes-core-rollout.js +90 -0
- package/dist/services/hermes-dashboard-endpoint.js +13 -0
- package/dist/services/hermes-plugin-source.js +149 -0
- package/dist/services/hermes-registry-service.js +258 -0
- package/dist/services/hermes-service.js +566 -0
- package/dist/services/hivemem-client.js +85 -0
- package/dist/services/host-local-writer.js +82 -0
- package/dist/services/incident-evidence.js +110 -0
- package/dist/services/incident-lessons.js +186 -0
- package/dist/services/llm-providers/anthropic.js +143 -0
- package/dist/services/llm-providers/index.js +7 -0
- package/dist/services/llm-providers/llama-cpp.js +237 -0
- package/dist/services/llm-providers/ollama.js +292 -0
- package/dist/services/llm-providers/openai-compatible.js +200 -0
- package/dist/services/llm-providers/provider-error.js +36 -0
- package/dist/services/llm-providers/registry.js +45 -0
- package/dist/services/llm-providers/types.js +2 -0
- package/dist/services/log-emitter.js +19 -0
- package/dist/services/log-retention.js +66 -0
- package/dist/services/maintenance-service.js +29 -0
- package/dist/services/managed-cron.js +493 -0
- package/dist/services/managed-ssh-admission.js +68 -0
- package/dist/services/monitor-pulse.js +10 -0
- package/dist/services/node-issue-monitor.js +71 -0
- package/dist/services/node-service.js +35 -0
- package/dist/services/notification-retention.js +51 -0
- package/dist/services/notification-service.js +94 -0
- package/dist/services/operational-observations.js +112 -0
- package/dist/services/operations-config.js +67 -0
- package/dist/services/ops-service.js +491 -0
- package/dist/services/orchestra-extras-service.js +370 -0
- package/dist/services/orchestra-resources-service.js +530 -0
- package/dist/services/os-scheduler-writer.js +73 -0
- package/dist/services/password-service.js +22 -0
- package/dist/services/prediction-service.js +284 -0
- package/dist/services/recovery-exercises.js +682 -0
- package/dist/services/remediation-service.js +325 -0
- package/dist/services/resource-enrollment.js +433 -0
- package/dist/services/resource-node-lock.js +18 -0
- package/dist/services/resource-participant-work.js +302 -0
- package/dist/services/role-service.js +388 -0
- package/dist/services/rollout-service.js +440 -0
- package/dist/services/security-export.js +185 -0
- package/dist/services/security-fusion-actions.js +22 -0
- package/dist/services/security-fusion-approvals.js +83 -0
- package/dist/services/security-fusion-evidence-integrity.js +144 -0
- package/dist/services/security-fusion-ingest.js +101 -0
- package/dist/services/security-fusion-outbox.js +185 -0
- package/dist/services/security-fusion-receipt-chain.js +134 -0
- package/dist/services/security-fusion-service.js +751 -0
- package/dist/services/security-fusion-verification.js +50 -0
- package/dist/services/self-update-service.js +200 -0
- package/dist/services/shared-resource-leases.js +413 -0
- package/dist/services/skill-publication-bundle.js +90 -0
- package/dist/services/skill-publication-settlement.js +36 -0
- package/dist/services/skill-publication-state.js +40 -0
- package/dist/services/skill-registry-service.js +327 -0
- package/dist/services/skill-service.js +148 -0
- package/dist/services/ssh-exec-service.js +139 -0
- package/dist/services/ssh-host-verification.js +27 -0
- package/dist/services/tailscale-node-key-service.js +75 -0
- package/dist/services/team-approvals.js +118 -0
- package/dist/services/topology-service.js +138 -0
- package/dist/services/transfer-authority-snapshot.js +151 -0
- package/dist/services/webhook-credential-migration.js +97 -0
- package/dist/services/webhook-security.js +242 -0
- package/dist/services/webhook-service.js +494 -0
- package/dist/types/auth.js +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/library.js +3 -0
- package/dist/types/notifications.js +3 -0
- package/dist/types/roles.js +3 -0
- package/dist/types/shared-contracts.js +3 -0
- package/dist/utils/env.js +141 -0
- package/dist/utils/heartbeat-peer.js +19 -0
- package/dist/utils/idempotency.js +45 -0
- package/dist/utils/ip.js +25 -0
- package/dist/utils/json-fields.js +94 -0
- package/dist/utils/local-tls.js +39 -0
- package/dist/utils/magnus-home.js +15 -0
- package/dist/utils/node-url.js +59 -0
- package/dist/utils/redact.js +26 -0
- package/dist/utils/safe-equal.js +13 -0
- package/dist/utils/sentinel-nodes.js +16 -0
- package/dist/utils/validate.js +41 -0
- package/package.json +119 -4
- package/prisma/migrations/20261031000000_baseline/migration.sql +2935 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +2185 -0
- 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
|