@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,474 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { MANAGED_CLAIM_HEADER, httpIdentity, httpReceiptBody, managedHttpRequestSchema } from '../security/managed-http.js';
|
|
3
|
+
import { nativeCompletionDescriptor, nativeCompletionResourceKey } from '../security/native-completion-registry.js';
|
|
4
|
+
import { COMMAND_COMPLETION_REGISTRY, COMMAND_COMPLETION_REGISTRY_SHA256, commandCompletionDescriptor } from '../security/command-completion-registry.js';
|
|
5
|
+
import { commandReconciliationSchema } from '../schemas/command-reconciliation.js';
|
|
6
|
+
import { admitResourceCommand, markResourceDispatched, failResourceCommand, settleResourceResult, settleResourceReconciliation } from '../services/command-resource-leases.js';
|
|
7
|
+
import { commandFenceSchema, commandResultSchema } from '../schemas/node-responses.js';
|
|
8
|
+
import { commandRequestDigest } from '../security/command-request.js';
|
|
9
|
+
import ky from 'ky';
|
|
10
|
+
import { HttpError } from '../middleware/error-handler.js';
|
|
11
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { RESULT_SIGNING_PREVIOUS_SECRET, RESULT_SIGNING_SECRET } from '../utils/env.js';
|
|
14
|
+
import { correlateSkillPublicationNoEffect } from '../services/skill-publication-settlement.js';
|
|
15
|
+
import { commandResultSigningPayload, verifyCommandResultSignature, } from '../security/command-result-signature.js';
|
|
16
|
+
/** Wire format shared with the node ledger; cross-wire tests guard canonicalization. */
|
|
17
|
+
export { commandReconciliationSchema } from '../schemas/command-reconciliation.js';
|
|
18
|
+
export function commandReconciliationSigningPayload(assertion) {
|
|
19
|
+
return JSON.stringify({ domain: 'magnus.command-reconciliation.v1', nodeFqdn: assertion.nodeFqdn,
|
|
20
|
+
commandId: assertion.commandId, requestSha256: assertion.requestSha256,
|
|
21
|
+
coordination: assertion.coordination ? { version: assertion.coordination.version,
|
|
22
|
+
resourceKey: assertion.coordination.resourceKey, fence: assertion.coordination.fence,
|
|
23
|
+
expiresAt: assertion.coordination.expiresAt } : null,
|
|
24
|
+
disposition: assertion.disposition, evidenceSha256: assertion.evidenceSha256,
|
|
25
|
+
approvalRef: assertion.approvalRef, decisionId: assertion.decisionId, expiresAt: assertion.expiresAt });
|
|
26
|
+
}
|
|
27
|
+
function verifiedReconciliation(raw, commandId, expected, fresh, nodeToken) {
|
|
28
|
+
const assertion = commandReconciliationSchema.parse(raw);
|
|
29
|
+
const secret = process.env['COMMAND_RECONCILIATION_SECRET'] ?? '';
|
|
30
|
+
if (!expected || !/^[a-f0-9]{64}$/.test(expected.requestSha256) || !expected.nodeFqdn
|
|
31
|
+
|| secret.length < 32 || secret === RESULT_SIGNING_SECRET || secret === nodeToken
|
|
32
|
+
|| assertion.commandId !== commandId || assertion.nodeFqdn !== expected.nodeFqdn
|
|
33
|
+
|| assertion.requestSha256 !== expected.requestSha256
|
|
34
|
+
|| (fresh && (assertion.expiresAt <= Date.now() || assertion.expiresAt > Date.now() + 300_000)))
|
|
35
|
+
throw new Error('Command reconciliation authority, expected node, digest, or validity differs');
|
|
36
|
+
const signature = createHmac('sha256', secret).update(commandReconciliationSigningPayload(assertion)).digest();
|
|
37
|
+
if (!timingSafeEqual(signature, Buffer.from(assertion.signature, 'hex')))
|
|
38
|
+
throw new Error('Command reconciliation signature verification failed');
|
|
39
|
+
return assertion;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* An agent running with MAGNUS_AGENT_PRIVILEGED=false answers root-only routes
|
|
43
|
+
* (cron, users, system updates, Ardent actions) with 403 AGENT_NOT_PRIVILEGED.
|
|
44
|
+
* Surface that as a non-retryable "feature disabled on this node" error.
|
|
45
|
+
*/
|
|
46
|
+
export const AGENT_NOT_PRIVILEGED = 'AGENT_NOT_PRIVILEGED';
|
|
47
|
+
async function rejectUnprivilegedAgent({ response }) {
|
|
48
|
+
if (response.status !== 403)
|
|
49
|
+
return;
|
|
50
|
+
let body;
|
|
51
|
+
try {
|
|
52
|
+
body = await response.clone().json();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (body?.error?.code !== AGENT_NOT_PRIVILEGED)
|
|
58
|
+
return;
|
|
59
|
+
const feature = typeof body.error.feature === 'string' ? body.error.feature.slice(0, 64) : 'feature';
|
|
60
|
+
throw new HttpError(409, AGENT_NOT_PRIVILEGED, `${feature} is disabled on this node: agent is not privileged (MAGNUS_AGENT_PRIVILEGED=false)`);
|
|
61
|
+
}
|
|
62
|
+
/** Whether an error means the target agent has the feature disabled. */
|
|
63
|
+
export function isAgentNotPrivileged(error) {
|
|
64
|
+
return error instanceof HttpError && error.code === AGENT_NOT_PRIVILEGED;
|
|
65
|
+
}
|
|
66
|
+
export class NodeApiClient {
|
|
67
|
+
api;
|
|
68
|
+
nodeToken;
|
|
69
|
+
defaultTimeout;
|
|
70
|
+
resourceContext;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.nodeToken = options.nodeToken;
|
|
73
|
+
this.defaultTimeout = options.timeout ?? 15_000;
|
|
74
|
+
if (options.resourceNodeId)
|
|
75
|
+
this.resourceContext = { nodeId: options.resourceNodeId,
|
|
76
|
+
actorId: options.resourceActorId ?? 'system:control-api', baseUrl: options.baseUrl,
|
|
77
|
+
...(options.resourceScope ? { scope: options.resourceScope } : {}) };
|
|
78
|
+
this.api = ky.create({
|
|
79
|
+
prefix: `${options.baseUrl}/api/v1`,
|
|
80
|
+
timeout: options.timeout ?? 15_000,
|
|
81
|
+
headers: {
|
|
82
|
+
'x-node-token': options.nodeToken,
|
|
83
|
+
},
|
|
84
|
+
hooks: { afterResponse: [rejectUnprivilegedAgent] },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** All finite native mutations use the same central owner and node execution slot. */
|
|
88
|
+
async mutateWithEvidence(method, path, body, timeout = this.defaultTimeout, commandId, skillPublication, requireManaged = false) {
|
|
89
|
+
if (!this.resourceContext)
|
|
90
|
+
throw new Error('Managed mutation requires canonical resource node identity');
|
|
91
|
+
const serialized = body === undefined ? '' : JSON.stringify(body);
|
|
92
|
+
const nativePath = '/' + path;
|
|
93
|
+
const requestedTimeoutSeconds = Math.ceil(timeout / 1000);
|
|
94
|
+
const timeoutSeconds = Math.min(3600, requestedTimeoutSeconds);
|
|
95
|
+
const descriptor = nativeCompletionDescriptor(method, nativePath);
|
|
96
|
+
const identity = httpIdentity(commandId ?? 'native-' + randomUUID(), method, nativePath, serialized, timeoutSeconds);
|
|
97
|
+
const admission = await admitResourceCommand(this.resourceContext, identity);
|
|
98
|
+
if (!admission && requireManaged)
|
|
99
|
+
throw new Error('This operation requires enrolled managed admission; direct node mutation is forbidden');
|
|
100
|
+
if (!admission)
|
|
101
|
+
return { response: await this.api(path, { method,
|
|
102
|
+
...(body === undefined ? {} : { body: serialized, headers: { 'content-type': 'application/json' } }), timeout, retry: 0 }).json() };
|
|
103
|
+
try {
|
|
104
|
+
if (!descriptor || descriptor.completionMode !== 'buffered-synchronous-http'
|
|
105
|
+
|| requestedTimeoutSeconds > descriptor.timeoutCeilingSeconds)
|
|
106
|
+
throw new Error('This enrolled-node mutation lacks a reviewed completion boundary; not dispatched');
|
|
107
|
+
if (admission.payload.coordination?.resourceKey !== nativeCompletionResourceKey(descriptor, this.resourceContext.nodeId))
|
|
108
|
+
throw new Error('Native mutation resource classification differs from its lease');
|
|
109
|
+
const capability = await this.api.get('ping').json();
|
|
110
|
+
if (!capability.success || capability.data?.features?.managedMutations !== 1)
|
|
111
|
+
throw new Error('Node does not support managed mutations; operation not dispatched');
|
|
112
|
+
await markResourceDispatched(admission);
|
|
113
|
+
const managedRequest = managedHttpRequestSchema.parse({
|
|
114
|
+
claim: { leaseId: admission.leaseId, nodeId: admission.nodeId, nodeFqdn: admission.nodeFqdn },
|
|
115
|
+
commandId: admission.commandId, method, path: '/' + path, body: serialized,
|
|
116
|
+
timeoutSeconds: identity.timeoutSeconds, coordination: admission.payload.coordination,
|
|
117
|
+
});
|
|
118
|
+
const envelope = await this.api.post('commands/managed-http', { retry: 0, timeout: timeout + 5000,
|
|
119
|
+
json: managedRequest }).json();
|
|
120
|
+
if (!envelope.success)
|
|
121
|
+
throw new Error('Managed mutation response was unsuccessful');
|
|
122
|
+
const { response } = envelope.data;
|
|
123
|
+
const receipt = this.verifiedCommandResult(envelope.data.receipt, admission.commandId);
|
|
124
|
+
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || typeof response.body !== 'string'
|
|
125
|
+
|| receipt.stdout !== httpReceiptBody(method, '/' + path, response.status, response.body))
|
|
126
|
+
throw new Error('Managed response body differs from signed receipt');
|
|
127
|
+
await settleResourceResult(admission.commandId, receipt);
|
|
128
|
+
if (response.status === 409 && skillPublication)
|
|
129
|
+
await correlateSkillPublicationNoEffect({
|
|
130
|
+
commandId: admission.commandId, requestSha256: commandRequestDigest(admission.payload),
|
|
131
|
+
skillName: skillPublication.skillName, version: skillPublication.version,
|
|
132
|
+
bundleSha256: skillPublication.bundleSha256, responseBody: response.body,
|
|
133
|
+
});
|
|
134
|
+
if (response.status >= 400)
|
|
135
|
+
throw new Error(`Native mutation completed with HTTP ${response.status}`);
|
|
136
|
+
const commandSha256 = receipt.coordination?.requestSha256;
|
|
137
|
+
if (!commandSha256 || commandSha256 !== commandRequestDigest(admission.payload))
|
|
138
|
+
throw new Error('Managed mutation command evidence differs');
|
|
139
|
+
return { response: JSON.parse(response.body), evidence: { commandId: receipt.commandId,
|
|
140
|
+
commandSha256, receiptSha256: createHash('sha256').update(JSON.stringify({
|
|
141
|
+
payload: commandResultSigningPayload(receipt), signature: receipt.signature,
|
|
142
|
+
})).digest('hex'),
|
|
143
|
+
acknowledgedAt: new Date() } };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
await failResourceCommand(admission);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async mutate(method, path, body, timeout = this.defaultTimeout) {
|
|
151
|
+
return (await this.mutateWithEvidence(method, path, body, timeout)).response;
|
|
152
|
+
}
|
|
153
|
+
async evidencedMutation(method, path, body, commandId) {
|
|
154
|
+
const result = await this.mutateWithEvidence(method, path, body, this.defaultTimeout, commandId);
|
|
155
|
+
if (!result.evidence)
|
|
156
|
+
throw new Error('Managed mutation acknowledgement requires a signed managed HTTP receipt');
|
|
157
|
+
return { response: result.response, evidence: result.evidence };
|
|
158
|
+
}
|
|
159
|
+
// ── Health ──
|
|
160
|
+
async collectHealth() {
|
|
161
|
+
return this.api.get('health/collect').json();
|
|
162
|
+
}
|
|
163
|
+
async enableSystemUpdates() {
|
|
164
|
+
return this.mutate('POST', 'health/system-updates/enable', undefined, 20 * 60_000);
|
|
165
|
+
}
|
|
166
|
+
// ── Commands ──
|
|
167
|
+
verifiedCommandResult(raw, commandId) {
|
|
168
|
+
const parsed = commandResultSchema.safeParse(raw);
|
|
169
|
+
if (!parsed.success || parsed.data.commandId !== commandId || !verifyCommandResultSignature(parsed.data, RESULT_SIGNING_SECRET, RESULT_SIGNING_PREVIOUS_SECRET))
|
|
170
|
+
throw new Error('Command result signature verification failed');
|
|
171
|
+
return parsed.data;
|
|
172
|
+
}
|
|
173
|
+
async executeCommand(payload) {
|
|
174
|
+
// Snapshot callers' mutable objects before any asynchronous admission check.
|
|
175
|
+
payload = structuredClone(payload);
|
|
176
|
+
if (!this.resourceContext)
|
|
177
|
+
throw new Error('Managed command requires canonical resource node identity');
|
|
178
|
+
const admission = await admitResourceCommand(this.resourceContext, payload);
|
|
179
|
+
if (admission)
|
|
180
|
+
payload = admission.payload;
|
|
181
|
+
try {
|
|
182
|
+
if (payload.coordination) {
|
|
183
|
+
commandFenceSchema.parse(payload.coordination);
|
|
184
|
+
const descriptor = commandCompletionDescriptor(payload);
|
|
185
|
+
const capability = await this.api.get('ping').json();
|
|
186
|
+
if (!capability.success || capability.data?.features?.commandFencing !== 1)
|
|
187
|
+
throw new Error('Node does not support command fencing; command not dispatched');
|
|
188
|
+
if (admission && capability.data?.features?.managedMutations !== 1)
|
|
189
|
+
throw new Error('Node does not support sticky managed mutation admission; command not dispatched');
|
|
190
|
+
if (descriptor && (capability.data?.features?.commandCompletionRegistry?.enabled !== 1
|
|
191
|
+
|| capability.data.features.commandCompletionRegistry.version !== COMMAND_COMPLETION_REGISTRY.version
|
|
192
|
+
|| capability.data.features.commandCompletionRegistry.sha256 !== COMMAND_COMPLETION_REGISTRY_SHA256))
|
|
193
|
+
throw new Error('Node command completion registry differs; command not dispatched');
|
|
194
|
+
if (payload.coordination.expiresAt <= Date.now())
|
|
195
|
+
throw new Error('Command lease expired; command not dispatched');
|
|
196
|
+
}
|
|
197
|
+
const timeout = (payload.timeoutSeconds + 5) * 1_000;
|
|
198
|
+
// A missing POST response is uncertain. Reconcile through the receipt API;
|
|
199
|
+
// never use transport retries to run an action again.
|
|
200
|
+
if (admission)
|
|
201
|
+
await markResourceDispatched(admission);
|
|
202
|
+
const response = await this.api.post('commands/execute', { json: payload, timeout, retry: 0, ...(admission ? { headers: { [MANAGED_CLAIM_HEADER]: JSON.stringify({ leaseId: admission.leaseId, nodeId: admission.nodeId, nodeFqdn: admission.nodeFqdn }) } } : {}) }).json();
|
|
203
|
+
const result = this.verifiedCommandResult(response?.data, payload.commandId);
|
|
204
|
+
const descriptor = commandCompletionDescriptor(payload);
|
|
205
|
+
if (descriptor && (result.completion?.registryVersion !== COMMAND_COMPLETION_REGISTRY.version
|
|
206
|
+
|| result.completion.registrySha256 !== COMMAND_COMPLETION_REGISTRY_SHA256
|
|
207
|
+
|| result.completion.descriptorId !== descriptor.id))
|
|
208
|
+
throw new Error('Command completion registry receipt differs');
|
|
209
|
+
if (!descriptor && result.completion !== undefined)
|
|
210
|
+
throw new Error('Unregistered command returned terminal registry evidence');
|
|
211
|
+
if (payload.coordination ? !result.coordination
|
|
212
|
+
|| result.coordination.resourceKey !== payload.coordination.resourceKey
|
|
213
|
+
|| result.coordination.fence !== payload.coordination.fence
|
|
214
|
+
|| result.coordination.expiresAt !== payload.coordination.expiresAt
|
|
215
|
+
|| result.coordination.requestSha256 !== commandRequestDigest(payload)
|
|
216
|
+
: result.coordination !== undefined)
|
|
217
|
+
throw new Error('Command result request/fence verification failed');
|
|
218
|
+
if (!response.success)
|
|
219
|
+
throw new Error('Command execution response was unsuccessful');
|
|
220
|
+
if (admission)
|
|
221
|
+
await settleResourceResult(payload.commandId, result);
|
|
222
|
+
return response;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (admission)
|
|
226
|
+
await failResourceCommand(admission);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
verifiedCommandReceipt(raw, commandId, expected) {
|
|
231
|
+
const parsed = z.object({ commandId: z.string(), requestSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
232
|
+
state: z.enum(['running', 'unknown', 'completed', 'reconciled']), result: z.unknown().optional(),
|
|
233
|
+
reconciliation: z.unknown().optional(), coordination: commandFenceSchema.optional(),
|
|
234
|
+
}).safeParse(raw);
|
|
235
|
+
if (!parsed.success || parsed.data.commandId !== commandId
|
|
236
|
+
|| (expected && parsed.data.requestSha256 !== expected.requestSha256))
|
|
237
|
+
throw new Error('Invalid command receipt');
|
|
238
|
+
const row = parsed.data;
|
|
239
|
+
if (row.state === 'reconciled') {
|
|
240
|
+
if (row.result !== undefined)
|
|
241
|
+
throw new Error('Reconciled receipt cannot contain a result');
|
|
242
|
+
const assertion = verifiedReconciliation(row.reconciliation, commandId, expected, false, this.nodeToken);
|
|
243
|
+
if (assertion.requestSha256 !== row.requestSha256
|
|
244
|
+
|| JSON.stringify(row.coordination ?? null) !== JSON.stringify(assertion.coordination ?? null))
|
|
245
|
+
throw new Error('Reconciliation receipt request/fence binding differs');
|
|
246
|
+
return { commandId, requestSha256: row.requestSha256, state: 'reconciled', reconciliation: assertion };
|
|
247
|
+
}
|
|
248
|
+
if (row.reconciliation !== undefined)
|
|
249
|
+
throw new Error('Unreconciled receipt cannot contain an attestation');
|
|
250
|
+
if (row.state === 'completed') {
|
|
251
|
+
const result = this.verifiedCommandResult(row.result, commandId);
|
|
252
|
+
if (result.coordination && result.coordination.requestSha256 !== row.requestSha256)
|
|
253
|
+
throw new Error('Receipt request binding differs');
|
|
254
|
+
return { commandId, requestSha256: row.requestSha256, state: 'completed', result };
|
|
255
|
+
}
|
|
256
|
+
if (row.result !== undefined)
|
|
257
|
+
throw new Error('Nonterminal receipt cannot contain a result');
|
|
258
|
+
return { commandId, requestSha256: row.requestSha256, state: row.state };
|
|
259
|
+
}
|
|
260
|
+
async getCommandReceipt(commandId, expected) {
|
|
261
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(commandId))
|
|
262
|
+
throw new Error('Invalid command identifier');
|
|
263
|
+
expected = expected ? { ...expected } : undefined;
|
|
264
|
+
const response = await this.api.get(`commands/receipts/${encodeURIComponent(commandId)}`).json();
|
|
265
|
+
if (!response.success)
|
|
266
|
+
throw new Error('Command receipt response was unsuccessful');
|
|
267
|
+
const receipt = this.verifiedCommandReceipt(response.data, commandId, expected);
|
|
268
|
+
if (this.resourceContext && receipt.state === 'completed')
|
|
269
|
+
await settleResourceResult(commandId, receipt.result);
|
|
270
|
+
if (this.resourceContext && receipt.state === 'reconciled')
|
|
271
|
+
await settleResourceReconciliation(this.resourceContext.nodeId, receipt.reconciliation);
|
|
272
|
+
return receipt;
|
|
273
|
+
}
|
|
274
|
+
/** Submit only an already signed, explicitly reviewed assertion. This method
|
|
275
|
+
* neither signs a decision, runs a command, nor retries ambiguous mutations. */
|
|
276
|
+
async reconcileCommandReceipt(raw, expected) {
|
|
277
|
+
expected = { ...expected };
|
|
278
|
+
const assertion = verifiedReconciliation(raw, raw.commandId, expected, true, this.nodeToken);
|
|
279
|
+
const capability = await this.api.get('ping').json();
|
|
280
|
+
if (!capability.success || capability.data?.features?.commandReconciliation !== 1)
|
|
281
|
+
throw new Error('Node does not support explicit command reconciliation');
|
|
282
|
+
// A capability lookup may consume the assertion's remaining validity.
|
|
283
|
+
verifiedReconciliation(assertion, assertion.commandId, expected, true, this.nodeToken);
|
|
284
|
+
const response = await this.api.post(`commands/receipts/${encodeURIComponent(assertion.commandId)}/reconcile`, { json: assertion, retry: 0 }).json();
|
|
285
|
+
if (!response.success)
|
|
286
|
+
throw new Error('Command reconciliation response was unsuccessful');
|
|
287
|
+
const receipt = this.verifiedCommandReceipt(response.data, assertion.commandId, expected);
|
|
288
|
+
if (receipt.state !== 'reconciled' || receipt.reconciliation.signature !== assertion.signature)
|
|
289
|
+
throw new Error('Node did not acknowledge the exact reconciliation decision');
|
|
290
|
+
if (this.resourceContext)
|
|
291
|
+
await settleResourceReconciliation(this.resourceContext.nodeId, receipt.reconciliation);
|
|
292
|
+
return receipt;
|
|
293
|
+
}
|
|
294
|
+
// ── Cron ──
|
|
295
|
+
async listCronJobs() {
|
|
296
|
+
return this.api.get('cron').json();
|
|
297
|
+
}
|
|
298
|
+
async listAllUserCronJobs() {
|
|
299
|
+
return this.api.get('cron/all-users').json();
|
|
300
|
+
}
|
|
301
|
+
async syncManagedCronJob(data) {
|
|
302
|
+
return this.evidencedMutation('POST', 'cron/managed-sync', data);
|
|
303
|
+
}
|
|
304
|
+
/** Pre-enrollment conversion may only stage a disabled definition and remove
|
|
305
|
+
* its same-name legacy wake-up. It deliberately cannot return activation evidence. */
|
|
306
|
+
async stageManagedCronForEnrollment(data) {
|
|
307
|
+
return this.api.post('cron/enrollment-stage', { json: data, retry: 0 }).json();
|
|
308
|
+
}
|
|
309
|
+
async activateManagedCronJob(data) {
|
|
310
|
+
return this.evidencedMutation('POST', 'cron/managed-activate', data);
|
|
311
|
+
}
|
|
312
|
+
async revokeManagedCronJob(data) {
|
|
313
|
+
return this.evidencedMutation('POST', 'cron/managed-revoke', data);
|
|
314
|
+
}
|
|
315
|
+
/** Signed evidence reconciliation is an enrollment control message. It does
|
|
316
|
+
* not execute or mutate the observed foreign scheduler itself. */
|
|
317
|
+
async reconcileSchedulerSource(data) {
|
|
318
|
+
return this.api.post('cron/source-reconciliation', { json: data, retry: 0 }).json();
|
|
319
|
+
}
|
|
320
|
+
async syncCronJob(data) {
|
|
321
|
+
return this.mutate('POST', 'cron/sync', data);
|
|
322
|
+
}
|
|
323
|
+
async updateCronJob(name, data) {
|
|
324
|
+
return this.mutate('PATCH', `cron/${encodeURIComponent(name)}`, data);
|
|
325
|
+
}
|
|
326
|
+
async deleteCronJob(name) {
|
|
327
|
+
return this.mutate('DELETE', `cron/${encodeURIComponent(name)}`);
|
|
328
|
+
}
|
|
329
|
+
// ── Files ──
|
|
330
|
+
async readFile(filePath) {
|
|
331
|
+
return this.api.get('files/read', { searchParams: { filePath } }).json();
|
|
332
|
+
}
|
|
333
|
+
async writeFile(payload) {
|
|
334
|
+
return this.mutate('POST', 'files/write', payload);
|
|
335
|
+
}
|
|
336
|
+
// ── Skills ──
|
|
337
|
+
async listSkills() {
|
|
338
|
+
return this.api.get('skills').json();
|
|
339
|
+
}
|
|
340
|
+
async installSkill(payload) {
|
|
341
|
+
return this.mutate('POST', 'skills/install', payload);
|
|
342
|
+
}
|
|
343
|
+
async publishSkillBundle(payload, commandId) {
|
|
344
|
+
const result = await this.mutateWithEvidence('POST', 'skills/install', payload, this.defaultTimeout, commandId, payload);
|
|
345
|
+
if (!result.evidence)
|
|
346
|
+
throw new Error('Managed skill publication acknowledgement requires a signed receipt');
|
|
347
|
+
return { response: result.response, evidence: result.evidence };
|
|
348
|
+
}
|
|
349
|
+
/** Ask the node to recover only the retained result of the exact previously
|
|
350
|
+
* dispatched skill publication. The node's recovery-claim path cannot invoke
|
|
351
|
+
* the skill handler or create a new effect. */
|
|
352
|
+
async recoverSkillBundle(payload, ownership) {
|
|
353
|
+
if (!this.resourceContext || this.resourceContext.nodeId !== ownership.nodeId)
|
|
354
|
+
throw new Error('Skill recovery target differs from canonical node identity');
|
|
355
|
+
const serialized = JSON.stringify(payload);
|
|
356
|
+
const identity = { ...httpIdentity(ownership.commandId, 'POST', '/skills/install', serialized, ownership.timeoutSeconds),
|
|
357
|
+
coordination: ownership.coordination };
|
|
358
|
+
if (commandRequestDigest(identity) !== ownership.requestSha256)
|
|
359
|
+
throw new Error('Skill recovery request differs from retained workflow identity');
|
|
360
|
+
const managedRequest = managedHttpRequestSchema.parse({
|
|
361
|
+
claim: { leaseId: ownership.leaseId, nodeId: ownership.nodeId, nodeFqdn: ownership.nodeFqdn },
|
|
362
|
+
commandId: ownership.commandId, method: 'POST', path: '/skills/install', body: serialized,
|
|
363
|
+
timeoutSeconds: ownership.timeoutSeconds, coordination: ownership.coordination, recoveryOnly: true,
|
|
364
|
+
});
|
|
365
|
+
const envelope = await this.api.post('commands/managed-http', { retry: 0,
|
|
366
|
+
timeout: Math.max(this.defaultTimeout, ownership.timeoutSeconds * 1000) + 5000,
|
|
367
|
+
json: managedRequest }).json();
|
|
368
|
+
if (!envelope.success)
|
|
369
|
+
throw new Error('Skill publication recovery response was unsuccessful');
|
|
370
|
+
const receipt = this.verifiedCommandResult(envelope.data.receipt, ownership.commandId);
|
|
371
|
+
const response = envelope.data.response;
|
|
372
|
+
if (!Number.isInteger(response.status) || typeof response.body !== 'string'
|
|
373
|
+
|| receipt.coordination?.requestSha256 !== ownership.requestSha256
|
|
374
|
+
|| receipt.stdout !== httpReceiptBody('POST', '/skills/install', response.status, response.body))
|
|
375
|
+
throw new Error('Recovered skill publication differs from its signed workflow receipt');
|
|
376
|
+
await settleResourceResult(ownership.commandId, receipt);
|
|
377
|
+
if (response.status === 409) {
|
|
378
|
+
await correlateSkillPublicationNoEffect({ commandId: ownership.commandId,
|
|
379
|
+
requestSha256: ownership.requestSha256, skillName: payload.skillName, version: payload.version,
|
|
380
|
+
bundleSha256: payload.bundleSha256, responseBody: response.body });
|
|
381
|
+
throw new Error('Skill publication recovery proved that no effect was committed');
|
|
382
|
+
}
|
|
383
|
+
if (response.status >= 400)
|
|
384
|
+
throw new Error(`Skill publication recovery completed with HTTP ${response.status}`);
|
|
385
|
+
return { response: JSON.parse(response.body), evidence: { commandId: ownership.commandId, commandSha256: ownership.requestSha256,
|
|
386
|
+
receiptSha256: createHash('sha256').update(JSON.stringify({
|
|
387
|
+
payload: commandResultSigningPayload(receipt), signature: receipt.signature,
|
|
388
|
+
})).digest('hex'), acknowledgedAt: new Date() } };
|
|
389
|
+
}
|
|
390
|
+
async removeSkill(skillName) {
|
|
391
|
+
return this.mutate('DELETE', `skills/${encodeURIComponent(skillName)}`);
|
|
392
|
+
}
|
|
393
|
+
// ── Self-update ──
|
|
394
|
+
async triggerUpdate(payload) {
|
|
395
|
+
return this.mutate('POST', 'update', payload);
|
|
396
|
+
}
|
|
397
|
+
// ── Ping ──
|
|
398
|
+
async ping() {
|
|
399
|
+
return this.api.get('ping').json();
|
|
400
|
+
}
|
|
401
|
+
// ── Hermes Agent runtime ──
|
|
402
|
+
async getHermesState(payload = {}) {
|
|
403
|
+
return this.api.get('hermes/state', { searchParams: payload }).json();
|
|
404
|
+
}
|
|
405
|
+
async installHermes(payload) {
|
|
406
|
+
return this.mutate('POST', 'hermes/install', payload, 20 * 60_000);
|
|
407
|
+
}
|
|
408
|
+
async configureHermes(payload) {
|
|
409
|
+
return this.mutate('POST', 'hermes/configure', payload, 60_000);
|
|
410
|
+
}
|
|
411
|
+
async updateHermes(payload) {
|
|
412
|
+
return this.mutate('POST', 'hermes/update', payload, 20 * 60_000);
|
|
413
|
+
}
|
|
414
|
+
async uninstallHermes(payload) {
|
|
415
|
+
return this.mutate('POST', 'hermes/uninstall', payload, 5 * 60_000);
|
|
416
|
+
}
|
|
417
|
+
// ── Codex CLI and ChatGPT OAuth ──
|
|
418
|
+
async getCodexState(refreshLatest = false) {
|
|
419
|
+
return this.api.get('codex/state', { searchParams: { refreshLatest }, timeout: 35_000 }).json();
|
|
420
|
+
}
|
|
421
|
+
// ── Ardent node-local runtime ──
|
|
422
|
+
async getArdentState(timeoutMs = 15_000) {
|
|
423
|
+
return this.api.get('ardent/state', { timeout: timeoutMs, retry: 0 }).json();
|
|
424
|
+
}
|
|
425
|
+
async getArdentProfileResource(profile, resource, query) {
|
|
426
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(profile) || !/^[A-Za-z0-9_/-]{1,120}$/.test(resource))
|
|
427
|
+
throw new Error('Invalid Ardent profile resource');
|
|
428
|
+
return this.api.get(`ardent/profiles/${profile}/api/${resource}`, { ...(query ? { searchParams: query } : {}), timeout: 15_000 }).json();
|
|
429
|
+
}
|
|
430
|
+
async getArdentArtifact(profile, artifactId) {
|
|
431
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(profile) || !/^[a-f0-9]{64}$/.test(artifactId))
|
|
432
|
+
throw new Error('Invalid Ardent artifact target');
|
|
433
|
+
const response = await this.api.get(`ardent/profiles/${profile}/api/artifacts/${artifactId}`, { timeout: 15_000 });
|
|
434
|
+
if (response.headers.get('content-type') !== 'application/octet-stream')
|
|
435
|
+
throw new Error('Ardent artifact response type differs');
|
|
436
|
+
const bytes = await response.arrayBuffer();
|
|
437
|
+
if (bytes.byteLength > 2 * 1024 * 1024)
|
|
438
|
+
throw new Error('Ardent artifact exceeds size limit');
|
|
439
|
+
return new Uint8Array(bytes);
|
|
440
|
+
}
|
|
441
|
+
async performArdentAction(profile, action, target, body) {
|
|
442
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(profile) || !/^[a-z-]{1,32}$/.test(action)
|
|
443
|
+
|| (target !== undefined && !/^[A-Za-z0-9_-]{1,128}$/.test(target)))
|
|
444
|
+
throw new Error('Invalid Ardent action target');
|
|
445
|
+
const timeout = action === 'package-install' ? 600_000 : action === 'profile-import-hermes' ? 90_000 : action.startsWith('service-') ? 115_000 : 45_000;
|
|
446
|
+
return this.mutate('POST', 'ardent/action', { profile, action, ...(target ? { target } : {}), body: body ?? {} }, timeout);
|
|
447
|
+
}
|
|
448
|
+
async updateCodex() {
|
|
449
|
+
return this.mutate('POST', 'codex/update', undefined, 11 * 60_000);
|
|
450
|
+
}
|
|
451
|
+
async logoutCodex() {
|
|
452
|
+
return this.mutate('POST', 'codex/logout', undefined, 35_000);
|
|
453
|
+
}
|
|
454
|
+
async startCodexDeviceAuth() {
|
|
455
|
+
return this.mutate('POST', 'codex/device-auth', undefined, 35_000);
|
|
456
|
+
}
|
|
457
|
+
async getCodexDeviceAuth(id) {
|
|
458
|
+
return this.api.get(`codex/device-auth/${encodeURIComponent(id)}`).json();
|
|
459
|
+
}
|
|
460
|
+
async cancelCodexDeviceAuth(id) {
|
|
461
|
+
return this.mutate('POST', `codex/device-auth/${encodeURIComponent(id)}/cancel`);
|
|
462
|
+
}
|
|
463
|
+
// ── User management ──
|
|
464
|
+
async listUsers() {
|
|
465
|
+
return this.api.get('users').json();
|
|
466
|
+
}
|
|
467
|
+
async provisionUser(payload) {
|
|
468
|
+
return this.mutate('POST', 'users/provision', payload);
|
|
469
|
+
}
|
|
470
|
+
async changeUserPassword(payload) {
|
|
471
|
+
return this.mutate('POST', 'users/change-password', payload);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
//# sourceMappingURL=node-api-client.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash, createPublicKey, timingSafeEqual, verify } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { HttpError } from '../middleware/error-handler.js';
|
|
5
|
+
const identifier = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,159}$/);
|
|
6
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
7
|
+
const payloadFields = {
|
|
8
|
+
proposal_id: identifier, proposal_revision: z.number().int().positive(),
|
|
9
|
+
plan_sha256: digest, decision: z.enum(['approve', 'reject']), decision_id: identifier,
|
|
10
|
+
decided_at: z.number().int().positive(), expires_at: z.number().int().positive(),
|
|
11
|
+
actor_binding_sha256: digest, execution_id: identifier.nullable(),
|
|
12
|
+
};
|
|
13
|
+
function validateBinding(p, context) {
|
|
14
|
+
if ((p.decision === 'approve') !== (p.execution_id !== null))
|
|
15
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: 'Approval execution binding is invalid' });
|
|
16
|
+
}
|
|
17
|
+
const v2PayloadSchema = z.object({ version: z.literal(2), ...payloadFields }).strict().superRefine(validateBinding);
|
|
18
|
+
const v3PayloadSchema = z.object({
|
|
19
|
+
version: z.literal(3), source_channel: z.enum(['telegram', 'iphone']), ...payloadFields,
|
|
20
|
+
}).strict().superRefine(validateBinding);
|
|
21
|
+
export const approvalPayloadSchema = z.union([v2PayloadSchema, v3PayloadSchema]);
|
|
22
|
+
export const approvalAssertionSchema = z.object({ payload: approvalPayloadSchema,
|
|
23
|
+
signature: z.string().regex(/^[A-Za-z0-9_-]{86}$/) }).strict();
|
|
24
|
+
/** Python ensure_ascii=True compatible canonical JSON; order is deliberate. */
|
|
25
|
+
export function approvalCanonicalJson(value) {
|
|
26
|
+
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, char => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
27
|
+
}
|
|
28
|
+
export function approvalPayloadCanonical(p) {
|
|
29
|
+
return approvalCanonicalJson({ version: p.version, ...(p.version === 3 ? { source_channel: p.source_channel } : {}), proposal_id: p.proposal_id, proposal_revision: p.proposal_revision,
|
|
30
|
+
plan_sha256: p.plan_sha256, decision: p.decision, decision_id: p.decision_id, decided_at: p.decided_at,
|
|
31
|
+
expires_at: p.expires_at, actor_binding_sha256: p.actor_binding_sha256, execution_id: p.execution_id });
|
|
32
|
+
}
|
|
33
|
+
export function approvalDigest(value) { return createHash('sha256').update(approvalCanonicalJson(value)).digest('hex'); }
|
|
34
|
+
export function verifyApprovalAssertion(raw) {
|
|
35
|
+
const parsed = approvalAssertionSchema.safeParse(raw);
|
|
36
|
+
const channel = parsed.success && parsed.data.payload.version === 3 ? parsed.data.payload.source_channel : 'telegram';
|
|
37
|
+
const actor = process.env[channel === 'iphone' ? 'APPROVAL_IPHONE_ACTOR_SHA256' : 'APPROVAL_TELEGRAM_ACTOR_SHA256'] ?? '';
|
|
38
|
+
if (!parsed.success || !/^[a-f0-9]{64}$/.test(actor))
|
|
39
|
+
throw new HttpError(403, 'APPROVAL_ASSERTION_REQUIRED', 'A configured, signed approval decision is required');
|
|
40
|
+
const { payload, signature } = parsed.data;
|
|
41
|
+
const canonical = approvalPayloadCanonical(payload);
|
|
42
|
+
let valid = false;
|
|
43
|
+
try {
|
|
44
|
+
const inline = process.env['APPROVAL_SIGNER_PUBLIC_KEY_PEM'];
|
|
45
|
+
const encoded = process.env['APPROVAL_SIGNER_PUBLIC_KEY_B64'];
|
|
46
|
+
const path = process.env['APPROVAL_SIGNER_PUBLIC_KEY_PATH'];
|
|
47
|
+
if (!inline && !encoded && !path)
|
|
48
|
+
throw new Error('missing key');
|
|
49
|
+
const key = createPublicKey(inline ?? (encoded
|
|
50
|
+
? { key: Buffer.from(encoded, 'base64'), format: 'der', type: 'spki' }
|
|
51
|
+
: readFileSync(path, 'utf8')));
|
|
52
|
+
valid = key.asymmetricKeyType === 'ed25519' && verify(null, Buffer.from(`magnus.approval-assertion.v${payload.version}\n` + canonical), key, Buffer.from(signature, 'base64url'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
valid = false;
|
|
56
|
+
}
|
|
57
|
+
if (!valid || !timingSafeEqual(Buffer.from(actor, 'hex'), Buffer.from(payload.actor_binding_sha256, 'hex')))
|
|
58
|
+
throw new HttpError(403, 'APPROVAL_ASSERTION_INVALID', 'Approval decision authentication failed');
|
|
59
|
+
return { payload, assertionSha256: createHash('sha256').update(canonical + '\n' + signature).digest('hex') };
|
|
60
|
+
}
|
|
61
|
+
export function requireFreshApproval(p, now = Date.now()) {
|
|
62
|
+
if (p.decided_at > now + 30_000 || p.expires_at <= now || p.decided_at >= p.expires_at
|
|
63
|
+
|| p.expires_at - p.decided_at > 15 * 60_000)
|
|
64
|
+
throw new HttpError(409, 'APPROVAL_ASSERTION_EXPIRED', 'Approval decision is expired or has an invalid lifetime');
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=approval-assertion.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Must remain byte-identical to the node command-completion registry. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
export const COMMAND_COMPLETION_REGISTRY = {
|
|
4
|
+
version: 1,
|
|
5
|
+
descriptors: [{
|
|
6
|
+
id: 'diagnostics-uptime-v1',
|
|
7
|
+
command: '/usr/bin/uptime',
|
|
8
|
+
args: [],
|
|
9
|
+
environment: {},
|
|
10
|
+
workingDirectory: null,
|
|
11
|
+
timeoutSeconds: 60,
|
|
12
|
+
resourceKeyPattern: '^node:[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$',
|
|
13
|
+
completionMode: 'supervised-cgroup-empty',
|
|
14
|
+
participantAuthority: {
|
|
15
|
+
allowedKinds: ['ssh-command'],
|
|
16
|
+
effectSemantics: 'read-only-node-diagnostic',
|
|
17
|
+
effectBoundary: 'bounded-node-process',
|
|
18
|
+
},
|
|
19
|
+
}],
|
|
20
|
+
};
|
|
21
|
+
export const COMMAND_COMPLETION_REGISTRY_SHA256 = createHash('sha256')
|
|
22
|
+
.update(JSON.stringify(COMMAND_COMPLETION_REGISTRY)).digest('hex');
|
|
23
|
+
export function commandCompletionDescriptor(input) {
|
|
24
|
+
const environment = input.environment ?? {};
|
|
25
|
+
return COMMAND_COMPLETION_REGISTRY.descriptors.find(descriptor => input.command === descriptor.command
|
|
26
|
+
&& JSON.stringify(input.args) === JSON.stringify(descriptor.args)
|
|
27
|
+
&& (input.workingDirectory ?? null) === descriptor.workingDirectory
|
|
28
|
+
&& JSON.stringify(Object.fromEntries(Object.entries(environment).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)))
|
|
29
|
+
=== JSON.stringify(descriptor.environment)
|
|
30
|
+
&& input.timeoutSeconds === descriptor.timeoutSeconds
|
|
31
|
+
&& input.coordination?.version === 1
|
|
32
|
+
&& new RegExp(descriptor.resourceKeyPattern).test(input.coordination.resourceKey)
|
|
33
|
+
&& /^[1-9][0-9]{0,18}$/.test(input.coordination.fence));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=command-completion-registry.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// Canonical wire digest mirrored by the independently packaged node agent.
|
|
3
|
+
// Cross-package fixtures guard compatibility without a runtime workspace import.
|
|
4
|
+
export function commandRequestDigest(input) {
|
|
5
|
+
return createHash('sha256').update(JSON.stringify({ commandId: input.commandId, command: input.command, args: input.args,
|
|
6
|
+
workingDirectory: input.workingDirectory ?? null,
|
|
7
|
+
environment: Object.fromEntries(Object.entries(input.environment ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
|
|
8
|
+
timeoutSeconds: input.timeoutSeconds, coordination: input.coordination ? {
|
|
9
|
+
version: 1, resourceKey: input.coordination.resourceKey, fence: input.coordination.fence, expiresAt: input.coordination.expiresAt,
|
|
10
|
+
} : null })).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=command-request.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export function commandResultSigningPayload(result) {
|
|
3
|
+
return JSON.stringify({
|
|
4
|
+
version: result.completion ? 3 : result.coordination ? 2 : 1,
|
|
5
|
+
commandId: result.commandId,
|
|
6
|
+
exitCode: result.exitCode,
|
|
7
|
+
durationMs: result.durationMs,
|
|
8
|
+
stdoutSha256: createHash('sha256').update(result.stdout).digest('hex'),
|
|
9
|
+
stderrSha256: createHash('sha256').update(result.stderr).digest('hex'),
|
|
10
|
+
truncated: result.truncated,
|
|
11
|
+
...(result.coordination ? { coordination: {
|
|
12
|
+
version: result.coordination.version, resourceKey: result.coordination.resourceKey,
|
|
13
|
+
fence: result.coordination.fence, expiresAt: result.coordination.expiresAt,
|
|
14
|
+
requestSha256: result.coordination.requestSha256,
|
|
15
|
+
} } : {}),
|
|
16
|
+
...(result.completion ? { completion: { registryVersion: result.completion.registryVersion,
|
|
17
|
+
registrySha256: result.completion.registrySha256, descriptorId: result.completion.descriptorId } } : {}),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function verifyCommandResultSignature(result, activeSecret, previousSecret = '') {
|
|
21
|
+
if (activeSecret.length < 32 || !/^[a-f0-9]{64}$/i.test(result.signature))
|
|
22
|
+
return false;
|
|
23
|
+
const actual = Buffer.from(result.signature, 'hex');
|
|
24
|
+
const candidates = [activeSecret, ...(previousSecret.length >= 32 ? [previousSecret] : [])];
|
|
25
|
+
let matched = false;
|
|
26
|
+
for (const secret of candidates) {
|
|
27
|
+
const expected = createHmac('sha256', secret)
|
|
28
|
+
.update(commandResultSigningPayload(result))
|
|
29
|
+
.digest();
|
|
30
|
+
matched = (actual.length === expected.length && timingSafeEqual(actual, expected)) || matched;
|
|
31
|
+
}
|
|
32
|
+
return matched;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=command-result-signature.js.map
|