@hivemagnus/magnus-server 1.1.12 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.env.example +5 -0
  2. package/README.md +4 -0
  3. package/agent-version.json +1 -1
  4. package/dist/cli/env-catalog.js +3 -0
  5. package/dist/config/env.js +4 -5
  6. package/dist/config/features.js +1 -1
  7. package/dist/db/generated/prisma/internal/class.js +4 -4
  8. package/dist/db/generated/prisma/internal/prismaNamespace.js +12 -1
  9. package/dist/db/generated/prisma/internal/prismaNamespaceBrowser.js +12 -1
  10. package/dist/db/generated/prisma/models/CindertraceResponse.js +2 -0
  11. package/dist/index.js +1 -16
  12. package/dist/openapi/spec.js +7 -2
  13. package/dist/queue/workers.js +0 -65
  14. package/dist/routes/cindertrace-response.js +183 -0
  15. package/dist/routes/heartbeat.js +1 -6
  16. package/dist/routes/security-fusion.js +15 -600
  17. package/dist/sdk/node-api-client.js +12 -0
  18. package/dist/security/cindertrace-response.js +14 -0
  19. package/dist/security/native-completion-registry.js +12 -0
  20. package/dist/server.js +3 -14
  21. package/dist/services/command-resource-leases.js +2 -4
  22. package/dist/services/security-export.js +4 -6
  23. package/package.json +3 -3
  24. package/prisma/migrations/20261103000000_cindertrace_response_bridge/migration-impact.json +128 -0
  25. package/prisma/migrations/20261103000000_cindertrace_response_bridge/migration.sql +6 -0
  26. package/prisma/schema.prisma +13 -0
  27. package/dist/services/security-fusion-actions.js +0 -22
  28. package/dist/services/security-fusion-evidence-integrity.js +0 -144
  29. package/dist/services/security-fusion-ingest.js +0 -101
  30. package/dist/services/security-fusion-outbox.js +0 -185
  31. package/dist/services/security-fusion-receipt-chain.js +0 -134
  32. package/dist/services/security-fusion-service.js +0 -751
  33. package/dist/services/security-fusion-verification.js +0 -50
@@ -172,7 +172,8 @@ export const ModelName = {
172
172
  ResourceDrainParticipant: 'ResourceDrainParticipant',
173
173
  ResourceParticipantWork: 'ResourceParticipantWork',
174
174
  ManagedCronDefinition: 'ManagedCronDefinition',
175
- ManagedCronOccurrence: 'ManagedCronOccurrence'
175
+ ManagedCronOccurrence: 'ManagedCronOccurrence',
176
+ CindertraceResponse: 'CindertraceResponse'
176
177
  };
177
178
  /**
178
179
  * Enums
@@ -1582,6 +1583,16 @@ export const ManagedCronOccurrenceScalarFieldEnum = {
1582
1583
  admissionJson: 'admissionJson',
1583
1584
  createdAt: 'createdAt'
1584
1585
  };
1586
+ export const CindertraceResponseScalarFieldEnum = {
1587
+ id: 'id',
1588
+ actionOperation: 'actionOperation',
1589
+ nodeId: 'nodeId',
1590
+ requestSha256: 'requestSha256',
1591
+ requestJson: 'requestJson',
1592
+ status: 'status',
1593
+ responseJson: 'responseJson',
1594
+ createdAt: 'createdAt'
1595
+ };
1585
1596
  export const SortOrder = {
1586
1597
  asc: 'asc',
1587
1598
  desc: 'desc'
@@ -144,7 +144,8 @@ export const ModelName = {
144
144
  ResourceDrainParticipant: 'ResourceDrainParticipant',
145
145
  ResourceParticipantWork: 'ResourceParticipantWork',
146
146
  ManagedCronDefinition: 'ManagedCronDefinition',
147
- ManagedCronOccurrence: 'ManagedCronOccurrence'
147
+ ManagedCronOccurrence: 'ManagedCronOccurrence',
148
+ CindertraceResponse: 'CindertraceResponse'
148
149
  };
149
150
  /*
150
151
  * Enums
@@ -1554,6 +1555,16 @@ export const ManagedCronOccurrenceScalarFieldEnum = {
1554
1555
  admissionJson: 'admissionJson',
1555
1556
  createdAt: 'createdAt'
1556
1557
  };
1558
+ export const CindertraceResponseScalarFieldEnum = {
1559
+ id: 'id',
1560
+ actionOperation: 'actionOperation',
1561
+ nodeId: 'nodeId',
1562
+ requestSha256: 'requestSha256',
1563
+ requestJson: 'requestJson',
1564
+ status: 'status',
1565
+ responseJson: 'responseJson',
1566
+ createdAt: 'createdAt'
1567
+ };
1557
1568
  export const SortOrder = {
1558
1569
  asc: 'asc',
1559
1570
  desc: 'desc'
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=CindertraceResponse.js.map
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ if (existsSync(envFile))
14
14
  // Validate required secrets before starting anything
15
15
  const { validateSecrets } = await import('./utils/env.js');
16
16
  validateSecrets();
17
- const { securityFusionLocal, securityFusionMode, startSecurityExporter, stopSecurityExporter } = await import('./services/security-export.js');
17
+ const { securityFusionMode, startSecurityExporter, stopSecurityExporter } = await import('./services/security-export.js');
18
18
  const { featureFlags } = await import('./config/features.js');
19
19
  const { serve } = await import('@hono/node-server');
20
20
  const { app } = await import('./server.js');
@@ -30,9 +30,6 @@ const { stopLogRetention } = await import('./services/log-retention.js');
30
30
  const { startHealthRetention } = await import('./services/health-retention.js');
31
31
  const { stopHealthRetention } = await import('./services/health-retention.js');
32
32
  const { startNotificationRetention, stopNotificationRetention } = await import('./services/notification-retention.js');
33
- const { startSecurityFusionRetention, stopSecurityFusionRetention } = await import('./services/security-fusion-service.js');
34
- const { reconcileSecurityFusionEvidence } = await import('./services/security-fusion-evidence-integrity.js');
35
- const { startSecurityFusionOutboxDispatcher, stopSecurityFusionOutboxDispatcher, } = await import('./services/security-fusion-outbox.js');
36
33
  const { startCommandWorker, closeCommandQueue } = await import('./queue/workers.js');
37
34
  const { startUpdateCheck, stopUpdateCheck } = await import('./services/update-check-service.js');
38
35
  const { startWebhookWorker, closeWebhookQueue } = await import('./services/webhook-service.js');
@@ -47,12 +44,6 @@ const port = Number(process.env['CONTROL_API_PORT'] ?? 4000);
47
44
  const features = featureFlags();
48
45
  console.log(`[startup] security-fusion=${securityFusionMode()} hermes-registry=${features.hermesRegistry ? 'on' : 'off'} `
49
46
  + `plugin-api=${features.pluginApi ? 'on' : 'off'} iam=${features.iam ? 'on' : 'off'}`);
50
- if (securityFusionLocal()) {
51
- const evidenceReconciliation = await reconcileSecurityFusionEvidence();
52
- console.log(`[security-fusion] evidence reconciliation checked=${evidenceReconciliation.checked} `
53
- + `updated=${evidenceReconciliation.updated} quarantined=${evidenceReconciliation.quarantined} `
54
- + `mismatched_cases=${evidenceReconciliation.mismatchedCases}`);
55
- }
56
47
  const { writeRuntimeFile } = await import('./utils/runtime-file.js');
57
48
  const { CONTROL_API_VERSION } = await import('./config/version.js');
58
49
  /** Background services shared by the TLS and development listeners. */
@@ -64,10 +55,6 @@ function startBackgroundServices(listenPort) {
64
55
  startLogRetention();
65
56
  startHealthRetention();
66
57
  startNotificationRetention();
67
- if (securityFusionLocal()) {
68
- startSecurityFusionRetention();
69
- startSecurityFusionOutboxDispatcher();
70
- }
71
58
  startSecurityExporter();
72
59
  startCommandWorker();
73
60
  startRolloutReconciler();
@@ -137,9 +124,7 @@ async function shutdown(signal) {
137
124
  stopLogRetention();
138
125
  stopHealthRetention();
139
126
  stopNotificationRetention();
140
- stopSecurityFusionRetention();
141
127
  stopSecurityExporter();
142
- stopSecurityFusionOutboxDispatcher();
143
128
  stopCodexFleetMonitor();
144
129
  stopUpdateCheck();
145
130
  stopExtensionSourceMonitor();
@@ -5,7 +5,7 @@ import { participantWorkReconciliationSchema, historicalParticipantWorkReconcili
5
5
  import { sharedResourceLeaseIdentitySchema, sharedResourceLeaseViewSchema, sharedResourceReconciliationSchema, sharedResourceReserveSchema, sharedResourceTerminalEvidenceSchema, } from '../schemas/shared-resource-lease.js';
6
6
  import { managedCronRegistrationSchema, schedulerSourceSettlementSchema } from '../schemas/managed-cron.js';
7
7
  import { legacyAdmissionRequestSchema, resourceClaimSchema } from '../security/managed-http.js';
8
- import { magnusObservationSchema } from '@hivemagnus/magnus-contracts';
8
+ import { magnusObservationSchema, responseRequestSchema } from '@hivemagnus/magnus-contracts';
9
9
  import { maintenanceSchema } from '../services/maintenance-service.js';
10
10
  import { rolloutInputSchema } from '../services/rollout-service.js';
11
11
  import { lessonSchema } from '../services/incident-lessons.js';
@@ -2703,6 +2703,7 @@ export function buildOpenApiSpec() {
2703
2703
  tags,
2704
2704
  components: {
2705
2705
  securitySchemes: {
2706
+ cindertraceSignature: { type: 'apiKey', in: 'header', name: 'x-cindertrace-signature', description: 'Dedicated response HMAC bound to method, path, exact body, x-cindertrace-time and x-cindertrace-nonce. Responses are signed against the same request binding.' },
2706
2707
  apiSecret: {
2707
2708
  type: 'apiKey',
2708
2709
  in: 'header',
@@ -2734,7 +2735,11 @@ export function buildOpenApiSpec() {
2734
2735
  },
2735
2736
  schemas: componentSchemas,
2736
2737
  },
2737
- paths,
2738
+ paths: { ...paths,
2739
+ '/api/v1/security-integration/response/execute': { post: { summary: 'Execute an exact Cindertrace-approved response through managed node admission', tags: ['Cindertrace'], security: [{ cindertraceSignature: [] }], requestBody: { required: true, content: { 'application/json': { schema: zSchema.toJSONSchema(responseRequestSchema) } } }, responses: { 200: { description: 'Signed completion receipt' }, 409: { description: 'Expired, already claimed, blocked, or uncertain execution' } } } },
2740
+ '/api/v1/security-integration/response/{id}': { get: { summary: 'Read an existing Cindertrace execution without replay', tags: ['Cindertrace'], security: [{ cindertraceSignature: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { 200: { description: 'Signed existing execution status' } } } },
2741
+ '/api/v1/security-integration/response/targets/{id}/health': { get: { summary: 'Read fresh target health for Cindertrace', tags: ['Cindertrace'], security: [{ cindertraceSignature: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { 200: { description: 'Signed minimal health snapshot' } } } },
2742
+ },
2738
2743
  };
2739
2744
  }
2740
2745
  //# sourceMappingURL=spec.js.map
@@ -1,11 +1,9 @@
1
1
  import { ResourceLeaseConflict } from '../services/command-resource-leases.js';
2
- import { securityFusionLocal } from '../services/security-export.js';
3
2
  import { recordMonitorPulse } from '../services/monitor-pulse.js';
4
3
  import { Queue, Worker } from 'bullmq';
5
4
  import { addDurableJob } from './requeue.js';
6
5
  import { validateRemediationCommandForDispatch } from '../services/remediation-service.js';
7
6
  import { reconcileCommands } from '../services/command-lifecycle.js';
8
- import { reconcileRestartVerification } from '../services/security-fusion-verification.js';
9
7
  import { getDb } from '../db/index.js';
10
8
  import { NodeApiClient } from '../sdk/index.js';
11
9
  import { getNodeBaseUrl } from '../utils/node-url.js';
@@ -14,7 +12,6 @@ import { RESULT_SIGNING_PREVIOUS_SECRET, RESULT_SIGNING_SECRET, getNodeManagerSe
14
12
  import { safeParse } from '../utils/validate.js';
15
13
  import { commandFenceSchema, commandResultSchema } from '../schemas/node-responses.js';
16
14
  import { incMetric } from '../routes/metrics.js';
17
- import { securityFusionActionSources } from '../services/security-fusion-actions.js';
18
15
  import { verifyCommandResultSignature } from '../security/command-result-signature.js';
19
16
  // ─── Command execution queue ───
20
17
  const REDIS_PREFIX = process.env['REDIS_PREFIX'] ?? 'magnus-';
@@ -69,59 +66,6 @@ export async function enqueueCommand(commandId) {
69
66
  removeOnFail: { age: 24 * 60 * 60, count: 1_000 },
70
67
  });
71
68
  }
72
- /**
73
- * Verify agent HMAC-SHA256 signature over command result fields.
74
- * Returns true if valid, false if tampered. Skips if secret is not configured.
75
- */
76
- async function recordSecurityFusionCommandResult(commandId, succeeded, detail) {
77
- const db = getDb();
78
- const outbox = await db.securityFusionCommandOutbox.findUnique({
79
- where: { remoteCommandId: commandId },
80
- });
81
- const action = outbox
82
- ? await db.securityFusionAction.findUnique({ where: { id: outbox.actionId } })
83
- : null;
84
- if (action && outbox) {
85
- if (succeeded && action.kind === 'service_restart') {
86
- const transitioned = await db.securityFusionAction.updateMany({
87
- where: { id: action.id, status: { in: securityFusionActionSources('verifying') } },
88
- data: { status: 'verifying', resultJson: JSON.stringify({ commandId, ...detail }) },
89
- });
90
- if (transitioned.count !== 1)
91
- return;
92
- await db.securityFusionCaseActivity.create({
93
- data: {
94
- caseId: action.caseId,
95
- kind: 'containment.restart_verifying',
96
- actorId: 'system:command-worker',
97
- detailJson: JSON.stringify({ actionId: action.id, commandId }),
98
- },
99
- });
100
- return;
101
- }
102
- const releasing = outbox.operation === 'release';
103
- const status = succeeded ? (releasing ? 'released' : 'succeeded') : 'failed';
104
- const transitioned = await db.securityFusionAction.updateMany({
105
- where: {
106
- id: action.id,
107
- status: {
108
- in: securityFusionActionSources(status),
109
- },
110
- },
111
- data: { status, resultJson: JSON.stringify({ commandId, ...detail }) },
112
- });
113
- if (transitioned.count !== 1)
114
- return;
115
- await db.securityFusionCaseActivity.create({
116
- data: {
117
- caseId: action.caseId,
118
- kind: `containment.${status}`,
119
- actorId: 'system:command-worker',
120
- detailJson: JSON.stringify({ actionId: action.id, commandId, ...detail }),
121
- },
122
- });
123
- }
124
- }
125
69
  /** Start the command execution worker. */
126
70
  export function startCommandWorker() {
127
71
  if (_commandWorker)
@@ -241,11 +185,6 @@ export function startCommandWorker() {
241
185
  });
242
186
  if (!recorded)
243
187
  return;
244
- if (securityFusionLocal())
245
- await recordSecurityFusionCommandResult(commandId, result.exitCode === 0, {
246
- exitCode: result.exitCode,
247
- durationMs: result.durationMs,
248
- });
249
188
  // Emit notification on command failure
250
189
  if (result.exitCode !== 0) {
251
190
  // Look up node name for the notification
@@ -287,8 +226,6 @@ export function startCommandWorker() {
287
226
  });
288
227
  if (!failed)
289
228
  throw err;
290
- if (securityFusionLocal())
291
- await recordSecurityFusionCommandResult(commandId, false, { error: errMsg });
292
229
  // Emit notification on worker-level failure
293
230
  const node = nodeId ? await db.node.findUnique({
294
231
  where: { id: nodeId },
@@ -321,8 +258,6 @@ export function startCommandWorker() {
321
258
  reconciliationBusy = true;
322
259
  try {
323
260
  await reconcileCommands();
324
- if (securityFusionLocal())
325
- await reconcileRestartVerification();
326
261
  recordMonitorPulse('commands');
327
262
  }
328
263
  catch {
@@ -0,0 +1,183 @@
1
+ import { bridgeSignature, equalSignature } from '../security/cindertrace-response.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { Hono } from 'hono';
4
+ import { responseRequestSchema } from '@hivemagnus/magnus-contracts';
5
+ import { getDb } from '../db/index.js';
6
+ import { NodeApiClient } from '../sdk/node-api-client.js';
7
+ import { getNodeBaseUrl } from '../utils/node-url.js';
8
+ import { getNodeManagerSecret } from '../utils/env.js';
9
+ import { HttpError } from '../middleware/error-handler.js';
10
+ import { logAuditEvent } from '../services/audit-service.js';
11
+ export const cindertraceResponseRouter = new Hono();
12
+ function secret() {
13
+ const value = process.env['CINDERTRACE_RESPONSE_SECRET'] ?? '';
14
+ if (value.length < 32 ||
15
+ value === process.env['API_SECRET'] ||
16
+ value === process.env['NODE_MANAGER_SECRET'])
17
+ throw new HttpError(503, 'RESPONSE_DISABLED', 'Dedicated Cindertrace response credential is required');
18
+ return value;
19
+ }
20
+ cindertraceResponseRouter.use('*', async (c, next) => {
21
+ const timestamp = c.req.header('x-cindertrace-time') ?? '', nonce = c.req.header('x-cindertrace-nonce') ?? '';
22
+ if (!/^\d{13}$/.test(timestamp) ||
23
+ Math.abs(Date.now() - Number(timestamp)) > 60000 ||
24
+ !/^[a-f0-9]{32}$/.test(nonce))
25
+ throw new HttpError(401, 'UNAUTHORIZED', 'Invalid response request');
26
+ const raw = c.req.method === 'GET' ? '' : await c.req.text();
27
+ if (Buffer.byteLength(raw) > 16384 ||
28
+ !equalSignature(c.req.header('x-cindertrace-signature') ?? '', bridgeSignature(secret(), c.req.method, c.req.path, timestamp, nonce, raw)))
29
+ throw new HttpError(401, 'UNAUTHORIZED', 'Invalid response request');
30
+ await next();
31
+ // Sign the exact response body and bind it to this request; no source output or credentials are returned.
32
+ const text = await c.res.clone().text();
33
+ c.header('x-cindertrace-signature', bridgeSignature(secret(), 'RESPONSE', c.req.path, timestamp, nonce, text));
34
+ c.header('Cache-Control', 'no-store');
35
+ });
36
+ async function client(nodeId) {
37
+ const node = await getDb().node.findUnique({ where: { id: nodeId } });
38
+ if (!node)
39
+ throw new HttpError(404, 'TARGET_NOT_FOUND', 'Target is unavailable');
40
+ return {
41
+ node,
42
+ api: new NodeApiClient({
43
+ baseUrl: getNodeBaseUrl(node.ipAddress, node.port),
44
+ nodeToken: getNodeManagerSecret(),
45
+ resourceNodeId: node.id,
46
+ resourceActorId: 'service:cindertrace',
47
+ }),
48
+ };
49
+ }
50
+ cindertraceResponseRouter.get('/targets/:id/health', async (c) => {
51
+ const { api } = await client(c.req.param('id'));
52
+ const health = await api.collectHealth();
53
+ return c.json({
54
+ data: {
55
+ services: health.data?.services ?? [],
56
+ wazuh: health.data?.wazuh,
57
+ responseCapable: await api.securityResponseCapable(),
58
+ },
59
+ });
60
+ });
61
+ export function validateResponsePolicy(p, node) {
62
+ if (p.operation === 'release')
63
+ return;
64
+ if (node.status !== 'ONLINE' ||
65
+ !node.lastHeartbeatAt ||
66
+ Date.now() - node.lastHeartbeatAt.getTime() > 120000)
67
+ throw new HttpError(409, 'TARGET_STALE', 'A fresh online target is required');
68
+ if (p.kind === 'service_restart') {
69
+ const allowed = (process.env['CINDERTRACE_RESTART_ALLOWLIST'] ?? '')
70
+ .split(',')
71
+ .map((x) => x.trim());
72
+ if (!p.serviceName || !allowed.includes(p.serviceName))
73
+ throw new HttpError(403, 'RESTART_BLOCKED', 'Service must be explicitly allowlisted');
74
+ }
75
+ else {
76
+ const tags = JSON.parse(node.tags), metadata = JSON.parse(node.metadata);
77
+ const protectedIds = (process.env['CINDERTRACE_PROTECTED_TARGET_IDS'] ?? '').split(',');
78
+ if (protectedIds.includes(node.id) ||
79
+ tags.some((x) => ['control-plane', 'network-infrastructure'].includes(x)) ||
80
+ metadata['securityIsolationPolicy'] !== 'allow' ||
81
+ metadata['securityIsolationCapable'] !== true ||
82
+ metadata['securityAegisChannelVerified'] !== true)
83
+ throw new HttpError(403, 'ISOLATION_BLOCKED', 'Explicit isolation policy, capability, and verified recovery channel are required');
84
+ }
85
+ }
86
+ cindertraceResponseRouter.post('/execute', async (c) => {
87
+ const parsed = responseRequestSchema.safeParse(await c.req.json());
88
+ if (!parsed.success)
89
+ throw new HttpError(400, 'INVALID_RESPONSE', 'Malformed response plan');
90
+ const p = parsed.data, raw = JSON.stringify(p), hash = createHash('sha256').update(raw).digest('hex');
91
+ const db = getDb();
92
+ const prior = await db.cindertraceResponse.findUnique({ where: { id: p.executionId } });
93
+ if (prior) {
94
+ if (prior.requestSha256 !== hash)
95
+ throw new HttpError(409, 'RESPONSE_CONFLICT', 'Execution identity is bound to another plan');
96
+ if (prior.status !== 'completed' || !prior.responseJson)
97
+ throw new HttpError(409, 'RESPONSE_UNKNOWN', 'Inspect the original execution; do not replay it');
98
+ return c.json(JSON.parse(prior.responseJson));
99
+ }
100
+ if (p.expiresAt <= Date.now() || p.expiresAt > Date.now() + 600000)
101
+ throw new HttpError(409, 'APPROVAL_EXPIRED', 'Approval is expired or outside its maximum lifetime');
102
+ const { node, api } = await client(p.nodeId);
103
+ validateResponsePolicy(p, node);
104
+ if (p.operation === 'release') {
105
+ const original = await db.cindertraceResponse.findUnique({
106
+ where: { actionOperation: `${p.actionId}:execute` },
107
+ });
108
+ if (!original ||
109
+ original.nodeId !== p.nodeId ||
110
+ JSON.parse(original.requestJson).kind !== 'host_isolation')
111
+ throw new HttpError(409, 'RELEASE_UNBOUND', 'Release requires an existing isolation execution on this target');
112
+ }
113
+ try {
114
+ await db.cindertraceResponse.create({
115
+ data: {
116
+ id: p.executionId,
117
+ actionOperation: `${p.actionId}:${p.operation}`,
118
+ nodeId: p.nodeId,
119
+ requestSha256: hash,
120
+ requestJson: raw,
121
+ },
122
+ });
123
+ }
124
+ catch (error) {
125
+ if (error.code === 'P2002')
126
+ throw new HttpError(409, 'RESPONSE_ALREADY_CLAIMED', 'Action operation already has an owner');
127
+ throw error;
128
+ }
129
+ await logAuditEvent({
130
+ action: 'cindertrace.response_claimed',
131
+ actorId: p.approvedBy,
132
+ targetType: 'node',
133
+ targetId: p.nodeId,
134
+ details: {
135
+ actionId: p.actionId,
136
+ executionId: p.executionId,
137
+ planSha256: p.planSha256,
138
+ operation: p.operation,
139
+ },
140
+ });
141
+ try {
142
+ const result = await api.securityResponse({
143
+ operation: p.operation,
144
+ kind: p.kind,
145
+ serviceName: p.serviceName,
146
+ isolationMinutes: p.isolationMinutes,
147
+ }, p.executionId);
148
+ if (!result.response.success || !Number.isInteger(result.response.data?.exitCode))
149
+ throw new Error('Response failed');
150
+ const reply = {
151
+ commandId: p.executionId,
152
+ actionId: p.actionId,
153
+ nodeId: p.nodeId,
154
+ operation: p.operation,
155
+ planSha256: p.planSha256,
156
+ exitCode: result.response.data.exitCode,
157
+ receiptSha256: result.evidence.receiptSha256,
158
+ };
159
+ await db.cindertraceResponse.update({
160
+ where: { id: p.executionId },
161
+ data: { status: 'completed', responseJson: JSON.stringify(reply) },
162
+ });
163
+ return c.json(reply);
164
+ }
165
+ catch {
166
+ await db.cindertraceResponse.update({
167
+ where: { id: p.executionId },
168
+ data: { status: 'unknown' },
169
+ });
170
+ throw new HttpError(409, 'RESPONSE_UNKNOWN', 'No verified completion; inspect the original execution');
171
+ }
172
+ });
173
+ cindertraceResponseRouter.get('/:id', async (c) => {
174
+ const row = await getDb().cindertraceResponse.findUnique({ where: { id: c.req.param('id') } });
175
+ if (!row)
176
+ throw new HttpError(404, 'NOT_FOUND', 'Execution is unavailable');
177
+ return c.json({
178
+ executionId: row.id,
179
+ status: row.status,
180
+ result: row.responseJson ? JSON.parse(row.responseJson) : null,
181
+ });
182
+ });
183
+ //# sourceMappingURL=cindertrace-response.js.map
@@ -1,4 +1,4 @@
1
- import { enqueueSecurityObservations, securityFusionLocal } from '../services/security-export.js';
1
+ import { enqueueSecurityObservations } from '../services/security-export.js';
2
2
  import { warnOnAgentVersionMismatch } from '../services/agent-version-warnings.js';
3
3
  import { recordSelfUpgradeReport } from '../services/agent-upgrade-service.js';
4
4
  import { Hono } from 'hono';
@@ -22,7 +22,6 @@ import { heartbeatHealthSchema } from '../schemas/node-responses.js';
22
22
  import { normalizeIpAddress, isLoopbackOrUnroutableIp, isUsableNodeIp } from '../utils/ip.js';
23
23
  import { AGENT_VERSION } from '../config/version.js';
24
24
  import { getHeartbeatStreamStats } from '../services/heartbeat-stream.js';
25
- import { ingestMagnusServiceHealth } from '../services/security-fusion-service.js';
26
25
  import { deriveAgentCertificateHostname, selectAgentEndpointHostname, verifyAgentCertificateHostname, } from '../utils/node-url.js';
27
26
  import { heartbeatPeerAddress } from '../utils/heartbeat-peer.js';
28
27
  import { authMiddleware } from '../middleware/auth.js';
@@ -490,10 +489,6 @@ heartbeatRouter.post('/', async (c) => {
490
489
  await enqueueSecurityObservations(tx, nodeId, existing?.name ?? fqdn ?? nodeId, (h.services ?? []), snapshot.id, snapshot.timestamp);
491
490
  return snapshot;
492
491
  });
493
- if (securityFusionLocal())
494
- void ingestMagnusServiceHealth(nodeId, existing?.name ?? fqdn ?? nodeId, (h.services ?? [])).catch((err) => {
495
- console.error(`[security-fusion] heartbeat conversion failed for ${nodeId}:`, err instanceof Error ? err.message : err);
496
- });
497
492
  // Evaluate health against alert rules (fire-and-forget)
498
493
  const alertNodeName = existing?.name ?? fqdn ?? nodeId;
499
494
  evaluateHealth(nodeId, alertNodeName, {