@hivemagnus/ardent 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/LICENSE +202 -0
- package/README.md +274 -2
- package/dist/a2a-client.d.ts +35 -0
- package/dist/a2a-client.js +172 -0
- package/dist/a2a-hermes-alias.d.ts +3 -0
- package/dist/a2a-hermes-alias.js +57 -0
- package/dist/a2a-idempotency.d.ts +13 -0
- package/dist/a2a-idempotency.js +86 -0
- package/dist/a2a-peers.d.ts +28 -0
- package/dist/a2a-peers.js +108 -0
- package/dist/a2a-push-inbox.d.ts +22 -0
- package/dist/a2a-push-inbox.js +84 -0
- package/dist/a2a-push.d.ts +27 -0
- package/dist/a2a-push.js +188 -0
- package/dist/a2a-server.d.ts +37 -0
- package/dist/a2a-server.js +186 -0
- package/dist/a2a-store.d.ts +10 -0
- package/dist/a2a-store.js +122 -0
- package/dist/a2a.d.ts +19 -0
- package/dist/a2a.js +466 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +381 -0
- package/dist/artifacts.d.ts +15 -0
- package/dist/artifacts.js +41 -0
- package/dist/backend-events.d.ts +49 -0
- package/dist/backend-events.js +8 -0
- package/dist/channels.d.ts +52 -0
- package/dist/channels.js +184 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +845 -0
- package/dist/codex-output-budget.d.ts +17 -0
- package/dist/codex-output-budget.js +31 -0
- package/dist/codex-output-budget.test.d.ts +1 -0
- package/dist/codex-output-budget.test.js +29 -0
- package/dist/codex.d.ts +82 -0
- package/dist/codex.js +431 -0
- package/dist/config.d.ts +171 -0
- package/dist/config.js +244 -0
- package/dist/context-checkpoints.d.ts +49 -0
- package/dist/context-checkpoints.js +248 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +101 -0
- package/dist/gateway-lock.d.ts +18 -0
- package/dist/gateway-lock.js +104 -0
- package/dist/hermes-compat.d.ts +15 -0
- package/dist/hermes-compat.js +62 -0
- package/dist/identity.d.ts +15 -0
- package/dist/identity.js +52 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/job-engine.d.ts +222 -0
- package/dist/job-engine.js +497 -0
- package/dist/job-news-handlers.d.ts +54 -0
- package/dist/job-news-handlers.js +153 -0
- package/dist/job-ownership.d.ts +9 -0
- package/dist/job-ownership.js +17 -0
- package/dist/json-schema.d.ts +9 -0
- package/dist/json-schema.js +20 -0
- package/dist/llamacpp.d.ts +49 -0
- package/dist/llamacpp.js +216 -0
- package/dist/magnus-control.d.ts +3 -0
- package/dist/magnus-control.js +99 -0
- package/dist/magnus-node-dependency-impact.d.ts +1 -0
- package/dist/magnus-node-dependency-impact.js +126 -0
- package/dist/magnus-owner-reads.d.ts +4 -0
- package/dist/magnus-owner-reads.js +99 -0
- package/dist/magnus-readiness.d.ts +1 -0
- package/dist/magnus-readiness.js +108 -0
- package/dist/mattermost.d.ts +18 -0
- package/dist/mattermost.js +303 -0
- package/dist/memory-tools.d.ts +5 -0
- package/dist/memory-tools.js +49 -0
- package/dist/memory.d.ts +40 -0
- package/dist/memory.js +128 -0
- package/dist/model-policy.d.ts +64 -0
- package/dist/model-policy.js +56 -0
- package/dist/news.d.ts +10 -0
- package/dist/news.js +291 -0
- package/dist/owner-read.d.ts +32 -0
- package/dist/owner-read.js +130 -0
- package/dist/plugins.d.ts +74 -0
- package/dist/plugins.js +115 -0
- package/dist/research.d.ts +110 -0
- package/dist/research.js +164 -0
- package/dist/runtime.d.ts +33 -0
- package/dist/runtime.js +665 -0
- package/dist/skills.d.ts +13 -0
- package/dist/skills.js +87 -0
- package/dist/store.d.ts +142 -0
- package/dist/store.js +322 -0
- package/dist/team-artifacts.d.ts +138 -0
- package/dist/team-artifacts.js +241 -0
- package/dist/team-child.d.ts +21 -0
- package/dist/team-child.js +130 -0
- package/dist/team-contract.d.ts +116 -0
- package/dist/team-contract.js +84 -0
- package/dist/team-control.d.ts +8 -0
- package/dist/team-control.js +152 -0
- package/dist/telegram.d.ts +35 -0
- package/dist/telegram.js +284 -0
- package/dist/tools.d.ts +48 -0
- package/dist/tools.js +58 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/model-policy.default.json +20 -0
- package/package.json +84 -4
- package/profiles/assistant/IDENTITY.md +9 -0
- package/profiles/assistant/SOUL.md +8 -0
- package/profiles/assistant/config.yaml +88 -0
- package/profiles/assistant/peers.yaml +12 -0
- package/profiles/assistant/skills/grounded-answers/SKILL.md +18 -0
- package/protocol/LICENSE-A2A.txt +202 -0
- package/protocol/README.md +12 -0
- package/protocol/a2a.proto +811 -0
- package/protocol/lock.json +9 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** Exact-node projection of the owning Control API's observed dependency graph. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { defineTool } from './tools.js';
|
|
5
|
+
const nodeId = z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
|
|
6
|
+
const count = z.number().int().nonnegative();
|
|
7
|
+
const graph = z.object({
|
|
8
|
+
discoveredAt: z.string().datetime(),
|
|
9
|
+
status: z.enum(['complete', 'partial']),
|
|
10
|
+
nodes: z.array(z.object({
|
|
11
|
+
id: nodeId,
|
|
12
|
+
name: z.string().min(1).max(160),
|
|
13
|
+
collection: z.object({
|
|
14
|
+
status: z.enum(['pass', 'unknown']),
|
|
15
|
+
observedAt: z.string().datetime(),
|
|
16
|
+
reason: z.string().nullable(),
|
|
17
|
+
}).passthrough(),
|
|
18
|
+
}).passthrough()).max(100),
|
|
19
|
+
edges: z.array(z.object({
|
|
20
|
+
from: nodeId,
|
|
21
|
+
to: nodeId,
|
|
22
|
+
service: z.string().min(1).max(160),
|
|
23
|
+
connectionCount: count,
|
|
24
|
+
}).passthrough()).max(2_000),
|
|
25
|
+
}).passthrough();
|
|
26
|
+
const relatedNode = z.object({ node_id: nodeId, name: z.string(), service: z.string(),
|
|
27
|
+
observed_connection_count: count, collection_status: z.enum(['pass', 'unknown']) }).strict();
|
|
28
|
+
const output = z.object({
|
|
29
|
+
source: z.string().url(), source_date: z.string().datetime().nullable(), observed_at: z.string().datetime(),
|
|
30
|
+
owner_response_sha256: z.string().regex(/^[a-f0-9]{64}$/), graph_discovered_at: z.string().datetime(),
|
|
31
|
+
graph_status: z.enum(['complete', 'partial']), node_id: nodeId, node_name: z.string(),
|
|
32
|
+
node_collection_status: z.enum(['pass', 'unknown']), node_collection_observed_at: z.string().datetime(),
|
|
33
|
+
node_collection_gap: z.string().nullable(), coverage: z.enum(['observed', 'partial']),
|
|
34
|
+
depends_on: z.array(relatedNode).max(2_000), dependents: z.array(relatedNode).max(2_000),
|
|
35
|
+
}).strict();
|
|
36
|
+
function ownerUrl(env) {
|
|
37
|
+
const base = env.ARDENT_MAGNUS_API_URL;
|
|
38
|
+
const token = env.ARDENT_MAGNUS_READ_TOKEN;
|
|
39
|
+
if (!base || !token)
|
|
40
|
+
throw Object.assign(new Error('Magnus read-only service binding is unavailable'), { code: 'SERVICE_BINDING_UNAVAILABLE' });
|
|
41
|
+
let url;
|
|
42
|
+
try {
|
|
43
|
+
url = new URL(base);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw Object.assign(new Error('Invalid Magnus service origin'), { code: 'INVALID_SERVICE_BINDING' });
|
|
47
|
+
}
|
|
48
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/' ||
|
|
49
|
+
(url.protocol === 'http:' && !['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))
|
|
50
|
+
throw Object.assign(new Error('Magnus bearer token requires an HTTPS origin outside loopback'), { code: 'INVALID_SERVICE_BINDING' });
|
|
51
|
+
url.pathname = '/api/v1/dependencies';
|
|
52
|
+
return { url, token };
|
|
53
|
+
}
|
|
54
|
+
async function readBoundedResponse(response) {
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw Object.assign(new Error(`Magnus dependency graph returned HTTP ${response.status}`), { code: 'SERVICE_READ_FAILED' });
|
|
57
|
+
if (Number(response.headers.get('content-length') ?? 0) > 512_000)
|
|
58
|
+
throw Object.assign(new Error('Magnus dependency graph exceeds limit'), { code: 'SERVICE_RESPONSE_TOO_LARGE' });
|
|
59
|
+
if (!response.body)
|
|
60
|
+
throw Object.assign(new Error('Magnus dependency graph has no body'), { code: 'SERVICE_READ_FAILED' });
|
|
61
|
+
const reader = response.body.getReader();
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let size = 0;
|
|
64
|
+
try {
|
|
65
|
+
for (;;) {
|
|
66
|
+
const part = await reader.read();
|
|
67
|
+
if (part.done)
|
|
68
|
+
break;
|
|
69
|
+
size += part.value.byteLength;
|
|
70
|
+
if (size > 512_000) {
|
|
71
|
+
await reader.cancel();
|
|
72
|
+
throw Object.assign(new Error('Magnus dependency graph exceeds limit'), { code: 'SERVICE_RESPONSE_TOO_LARGE' });
|
|
73
|
+
}
|
|
74
|
+
chunks.push(Buffer.from(part.value));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
return Buffer.concat(chunks, size);
|
|
81
|
+
}
|
|
82
|
+
async function readNodeImpact(env, requestedNodeId, signal) {
|
|
83
|
+
const { url, token } = ownerUrl(env);
|
|
84
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
85
|
+
signal, redirect: 'manual' });
|
|
86
|
+
const raw = await readBoundedResponse(response);
|
|
87
|
+
const data = z.object({ success: z.literal(true), data: graph }).passthrough()
|
|
88
|
+
.parse(JSON.parse(raw.toString('utf8'))).data;
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const discovered = Date.parse(data.discoveredAt);
|
|
91
|
+
if (discovered < now - 630_000 || discovered > now + 5_000)
|
|
92
|
+
throw Object.assign(new Error('Magnus dependency observation is stale or future dated'), { code: 'SOURCE_OBSERVATION_STALE' });
|
|
93
|
+
const nodes = new Map(data.nodes.map(item => [item.id, item]));
|
|
94
|
+
if (nodes.size !== data.nodes.length)
|
|
95
|
+
throw Object.assign(new Error('Magnus dependency graph contains duplicate node IDs'), { code: 'SOURCE_IDENTITY_MISMATCH' });
|
|
96
|
+
const requested = nodes.get(requestedNodeId);
|
|
97
|
+
if (!requested)
|
|
98
|
+
throw Object.assign(new Error('Requested node is absent from the observed dependency graph'), { code: 'SOURCE_NODE_UNAVAILABLE' });
|
|
99
|
+
if (data.edges.some(item => !nodes.has(item.from) || !nodes.has(item.to)))
|
|
100
|
+
throw Object.assign(new Error('Magnus dependency graph contains an unknown node reference'), { code: 'SOURCE_IDENTITY_MISMATCH' });
|
|
101
|
+
const project = (relatedId, service, connections) => {
|
|
102
|
+
const related = nodes.get(relatedId);
|
|
103
|
+
return { node_id: related.id, name: related.name, service,
|
|
104
|
+
observed_connection_count: connections, collection_status: related.collection.status };
|
|
105
|
+
};
|
|
106
|
+
const sort = (a, b) => a.node_id.localeCompare(b.node_id) || a.service.localeCompare(b.service);
|
|
107
|
+
const dependsOn = data.edges.filter(item => item.from === requestedNodeId)
|
|
108
|
+
.map(item => project(item.to, item.service, item.connectionCount)).sort(sort);
|
|
109
|
+
const dependents = data.edges.filter(item => item.to === requestedNodeId)
|
|
110
|
+
.map(item => project(item.from, item.service, item.connectionCount)).sort(sort);
|
|
111
|
+
const sourceDate = response.headers.get('date');
|
|
112
|
+
return output.parse({ source: url.toString(),
|
|
113
|
+
source_date: sourceDate && Number.isFinite(Date.parse(sourceDate)) ? new Date(sourceDate).toISOString() : null,
|
|
114
|
+
observed_at: new Date(now).toISOString(), owner_response_sha256: createHash('sha256').update(raw).digest('hex'),
|
|
115
|
+
graph_discovered_at: data.discoveredAt, graph_status: data.status, node_id: requested.id, node_name: requested.name,
|
|
116
|
+
node_collection_status: requested.collection.status, node_collection_observed_at: requested.collection.observedAt,
|
|
117
|
+
node_collection_gap: requested.collection.status === 'unknown' ? 'collection_unavailable' : null,
|
|
118
|
+
coverage: data.status === 'complete' && requested.collection.status === 'pass' ? 'observed' : 'partial',
|
|
119
|
+
depends_on: dependsOn, dependents });
|
|
120
|
+
}
|
|
121
|
+
export function magnusNodeDependencyImpactTool(env) {
|
|
122
|
+
return defineTool({ name: 'magnus_node_dependency_impact',
|
|
123
|
+
description: 'Read observed direct dependency edges for one exact Magnus node. Missing or partial collection is explicit; this is not a service inventory, causal proof, or remediation authority.',
|
|
124
|
+
input: z.object({ node_id: nodeId }).strict(), output, effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 15_000,
|
|
125
|
+
handler: async ({ node_id }, signal) => readNodeImpact(env, node_id, signal) });
|
|
126
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Read-only Magnus Control API observations ported from the retired Hermes `magnus` plugin. No command, log-query, or remediation authority. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { defineTool } from './tools.js';
|
|
4
|
+
import { ownerReadOutput, ownerReadResult, readOwnedApi, withQuery } from './owner-read.js';
|
|
5
|
+
const id = z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
|
|
6
|
+
const bounded = (min, max) => z.number().int().min(min).max(max);
|
|
7
|
+
const text = z.string().min(1).max(200);
|
|
8
|
+
const API = '/api/v1';
|
|
9
|
+
const str = (input, key) => typeof input[key] === 'string' ? input[key] : undefined;
|
|
10
|
+
const num = (input, key) => typeof input[key] === 'number' ? input[key] : undefined;
|
|
11
|
+
const fleet = (name, description, path) => ({ name, description, input: z.object({}).strict(), paths: () => [`${API}/${path}`] });
|
|
12
|
+
const specs = [
|
|
13
|
+
{ name: 'magnus_fleet_overview', description: 'Read Magnus fleet statistics and the latest fleet check run.', input: z.object({}).strict(),
|
|
14
|
+
paths: () => [`${API}/stats/overview`, `${API}/fleet-checks/latest`],
|
|
15
|
+
combine: values => ({ stats: values[0], latest_fleet_check: values[1] }) },
|
|
16
|
+
fleet('magnus_codex_fleet_status', 'Read cached per-installation Codex versions, available updates, and ChatGPT OAuth state.', 'ops/codex-fleet-status'),
|
|
17
|
+
{ name: 'magnus_list_nodes', description: 'List Magnus inventory nodes with optional status, text query, or tag filters.',
|
|
18
|
+
input: z.object({ status: text.optional(), query: text.optional(), tag: text.optional(), page_size: bounded(1, 100).optional() }).strict(),
|
|
19
|
+
paths: input => [withQuery(`${API}/nodes`, { status: str(input, 'status'), query: str(input, 'query'), tag: str(input, 'tag'), pageSize: num(input, 'page_size') ?? 50 })],
|
|
20
|
+
rowLimit: input => num(input, 'page_size') ?? 50 },
|
|
21
|
+
{ name: 'magnus_get_node', description: 'Read one Magnus inventory node record.', node: true, input: z.object({ node_id: id }).strict(),
|
|
22
|
+
paths: input => [`${API}/nodes/${str(input, 'node_id')}`] },
|
|
23
|
+
{ name: 'magnus_hermes_installations', description: 'List retained Hermes/Ardent installation observations, optionally filtered by status, node, or hostname substring.',
|
|
24
|
+
input: z.object({ status: text.optional(), node_id: id.optional(), hostname: text.optional(), installed_only: z.boolean().optional(), page_size: bounded(1, 100).optional() }).strict(),
|
|
25
|
+
paths: () => [`${API}/hermes/installations`],
|
|
26
|
+
combine: (values, input) => {
|
|
27
|
+
const rows = z.array(z.record(z.string(), z.unknown())).parse(values[0]);
|
|
28
|
+
const host = str(input, 'hostname')?.toLowerCase();
|
|
29
|
+
return rows.filter(row => (!str(input, 'status') || row['status'] === str(input, 'status')) &&
|
|
30
|
+
(!str(input, 'node_id') || row['nodeId'] === str(input, 'node_id')) &&
|
|
31
|
+
(!host || String(row['hostname'] ?? '').toLowerCase().includes(host)) &&
|
|
32
|
+
(input['installed_only'] !== true || row['status'] !== 'not_installed')).slice(0, num(input, 'page_size') ?? 50);
|
|
33
|
+
},
|
|
34
|
+
rowLimit: input => num(input, 'page_size') ?? 50 },
|
|
35
|
+
{ name: 'magnus_hermes_installation', description: 'Read one retained Hermes/Ardent installation observation by exact installation ID.', input: z.object({ installation_id: id }).strict(),
|
|
36
|
+
paths: input => [`${API}/hermes/installations/${str(input, 'installation_id')}`] },
|
|
37
|
+
{ name: 'magnus_list_anomalies', description: 'List fleet anomalies, optionally by resolved state, severity, or node.',
|
|
38
|
+
input: z.object({ resolved: z.boolean().optional(), severity: z.enum(['info', 'warning', 'critical']).optional(), node_id: id.optional(), page_size: bounded(1, 100).optional() }).strict(),
|
|
39
|
+
paths: input => [withQuery(`${API}/anomalies`, { resolved: typeof input['resolved'] === 'boolean' ? input['resolved'] : undefined, severity: str(input, 'severity'), nodeId: str(input, 'node_id'), pageSize: num(input, 'page_size') ?? 25 })],
|
|
40
|
+
rowLimit: input => num(input, 'page_size') ?? 25 },
|
|
41
|
+
{ name: 'magnus_fleet_check_runs', description: 'List recent Magnus fleet check runs.', input: z.object({ kind: text.optional(), limit: bounded(1, 100).optional() }).strict(),
|
|
42
|
+
paths: input => [withQuery(`${API}/fleet-checks/runs`, { kind: str(input, 'kind'), limit: num(input, 'limit') ?? 10 })], rowLimit: input => num(input, 'limit') ?? 10 },
|
|
43
|
+
{ name: 'magnus_cron_jobs', description: 'List managed cron job observations, optionally for one node.', input: z.object({ node_id: id.optional() }).strict(),
|
|
44
|
+
paths: input => [withQuery(`${API}/cron`, { nodeId: str(input, 'node_id') })] },
|
|
45
|
+
fleet('magnus_dependency_health', 'Read the fleet dependency-health summary.', 'ops/dependency-health'),
|
|
46
|
+
fleet('magnus_wazuh_fleet_status', 'Read the fleet Wazuh agent status summary.', 'nodes/wazuh/summary'),
|
|
47
|
+
fleet('magnus_system_update_status', 'Read the fleet operating-system update status summary.', 'nodes/system-updates/summary'),
|
|
48
|
+
{ name: 'magnus_health_history', description: 'Read bounded health snapshot history for one node.', node: true,
|
|
49
|
+
input: z.object({ node_id: id, limit: bounded(1, 500).optional() }).strict(),
|
|
50
|
+
paths: input => [withQuery(`${API}/nodes/${str(input, 'node_id')}/health/history`, { limit: num(input, 'limit') ?? 100 })], rowLimit: input => num(input, 'limit') ?? 100 },
|
|
51
|
+
{ name: 'magnus_active_alerts', description: 'List active alert-rule violations, optionally for one node.', input: z.object({ node_id: id.optional(), page_size: bounded(1, 100).optional() }).strict(),
|
|
52
|
+
bare: true, paths: input => [withQuery(`${API}/alerts/violations`, { nodeId: str(input, 'node_id'), status: 'active', pageSize: num(input, 'page_size') ?? 50 })],
|
|
53
|
+
combine: values => z.object({ violations: z.array(z.unknown()), total: z.number().int().nonnegative() }).passthrough().parse(values[0]).violations,
|
|
54
|
+
rowLimit: input => num(input, 'page_size') ?? 50 },
|
|
55
|
+
{ name: 'magnus_recent_commands', description: 'List recent Magnus command lifecycle records. Command text, arguments, and output are redacted.',
|
|
56
|
+
input: z.object({ node_id: id.optional(), status: z.enum(['PENDING', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', 'TIMEOUT', 'CANCELLED']).optional(), hours: bounded(1, 720).optional(), page_size: bounded(1, 100).optional() }).strict(),
|
|
57
|
+
paths: input => [withQuery(`${API}/commands`, { nodeId: str(input, 'node_id'), status: str(input, 'status'), hours: num(input, 'hours') ?? 24, pageSize: num(input, 'page_size') ?? 25 })],
|
|
58
|
+
rowLimit: input => num(input, 'page_size') ?? 25 },
|
|
59
|
+
{ name: 'magnus_audit_events', description: 'List Magnus audit events, optionally by date, action, or actor.',
|
|
60
|
+
input: z.object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), action: text.optional(), actor_id: text.optional(), page_size: bounded(1, 100).optional() }).strict(),
|
|
61
|
+
paths: input => [withQuery(`${API}/audit`, { date: str(input, 'date'), action: str(input, 'action'), actorId: str(input, 'actor_id'), pageSize: num(input, 'page_size') ?? 25 })],
|
|
62
|
+
rowLimit: input => num(input, 'page_size') ?? 25 },
|
|
63
|
+
{ name: 'magnus_fleet_timeline', description: 'Read the merged recent fleet event timeline.', input: z.object({ limit: bounded(1, 200).optional() }).strict(),
|
|
64
|
+
paths: input => [withQuery(`${API}/ops/timeline`, { limit: num(input, 'limit') ?? 80 })] },
|
|
65
|
+
{ name: 'magnus_incident_summary', description: 'Read an incident summary for a bounded window, optionally for one node.', input: z.object({ node_id: id.optional(), hours: bounded(1, 168).optional() }).strict(),
|
|
66
|
+
paths: input => [withQuery(`${API}/ops/incident-summary`, { nodeId: str(input, 'node_id'), hours: num(input, 'hours') ?? 24 })] },
|
|
67
|
+
fleet('magnus_hermes_effective_state', 'Read effective Hermes/Ardent configuration state per installation.', 'ops/hermes-effective-state'),
|
|
68
|
+
fleet('magnus_package_security_posture', 'Read the fleet package security posture.', 'ops/package-security-posture'),
|
|
69
|
+
{ name: 'magnus_correlate_anomalies', description: 'Group anomalies that occurred within a time window.', input: z.object({ window_minutes: bounded(1, 120).optional(), include_resolved: z.boolean().optional() }).strict(),
|
|
70
|
+
paths: input => [withQuery(`${API}/anomalies/correlations`, { windowMinutes: num(input, 'window_minutes') ?? 5, includeResolved: input['include_resolved'] === true })] },
|
|
71
|
+
{ name: 'magnus_anomaly_timeline', description: 'Read the anomaly timeline for a bounded window, optionally for one node.', input: z.object({ node_id: id.optional(), hours: bounded(1, 720).optional() }).strict(),
|
|
72
|
+
paths: input => [withQuery(`${API}/anomalies/timeline`, { nodeId: str(input, 'node_id'), hours: num(input, 'hours') ?? 24 })] },
|
|
73
|
+
fleet('magnus_fleet_predictions', 'Read fleet capacity and failure predictions.', 'predictions'),
|
|
74
|
+
{ name: 'magnus_node_predictions', description: 'Read capacity and failure predictions for one node.', node: true, input: z.object({ node_id: id }).strict(),
|
|
75
|
+
paths: input => [`${API}/predictions/${str(input, 'node_id')}`] },
|
|
76
|
+
fleet('magnus_skill_catalog', 'Read the Magnus skill catalogue.', 'skills'),
|
|
77
|
+
{ name: 'magnus_skill_assignment_drift', description: 'List installations whose assigned skills drift from the catalogue.', input: z.object({}).strict(),
|
|
78
|
+
paths: () => [`${API}/ops/hermes-effective-state`],
|
|
79
|
+
combine: values => {
|
|
80
|
+
const state = z.object({ installations: z.array(z.record(z.string(), z.unknown())).default([]) }).passthrough().parse(values[0]);
|
|
81
|
+
const drifted = state.installations.filter(item => Array.isArray(item['driftedSkills']) && item['driftedSkills'].length > 0)
|
|
82
|
+
.map(item => ({ installation_id: item['id'], hostname: item['hostname'], status: item['status'], skills: item['driftedSkills'] }));
|
|
83
|
+
return { drifted_installations: drifted.length, installations: drifted };
|
|
84
|
+
} }
|
|
85
|
+
];
|
|
86
|
+
export const MAGNUS_OWNER_READ_TOOLS = new Set(specs.map(spec => spec.name));
|
|
87
|
+
export const MAGNUS_NODE_OWNER_READ_TOOLS = new Set(specs.filter(spec => spec.node).map(spec => spec.name));
|
|
88
|
+
export function magnusOwnerReadTools(env) {
|
|
89
|
+
return specs.map(spec => defineTool({
|
|
90
|
+
name: spec.name, description: `${spec.description} Owner observation only; it authorizes no command, deployment, or remediation.`,
|
|
91
|
+
input: spec.input, output: ownerReadOutput, effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 20_000,
|
|
92
|
+
handler: async (input, signal) => {
|
|
93
|
+
const request = input;
|
|
94
|
+
const reads = await Promise.all(spec.paths(request).map(path => readOwnedApi('ARDENT_MAGNUS_API_URL', 'ARDENT_MAGNUS_READ_TOKEN', path, env, signal, {}, undefined, spec.bare === true)));
|
|
95
|
+
const data = spec.combine ? spec.combine(reads.map(read => read.value), request) : reads[0]?.value;
|
|
96
|
+
return ownerReadResult(reads, data, spec.rowLimit?.(request));
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function magnusFleetReadinessTool(env: NodeJS.ProcessEnv): import("./tools.js").Tool;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Bounded projection of Magnus-owned fleet readiness; no action authority. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { defineTool } from './tools.js';
|
|
4
|
+
const count = z.number().int().nonnegative();
|
|
5
|
+
const node = z.object({
|
|
6
|
+
id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/),
|
|
7
|
+
name: z.string().min(1).max(160),
|
|
8
|
+
hostname: z.string().max(253).nullable(),
|
|
9
|
+
operationalStatus: z.string().min(1).max(40),
|
|
10
|
+
heartbeatAgeMs: count.nullable(),
|
|
11
|
+
stale: z.boolean(),
|
|
12
|
+
health: z.object({
|
|
13
|
+
timestamp: z.string().datetime(),
|
|
14
|
+
cpuUsagePercent: z.number().finite().nullable(),
|
|
15
|
+
memoryPercent: z.number().finite().nullable(),
|
|
16
|
+
diskWarnings: z.array(z.object({ mountPoint: z.string(), usagePercent: z.number() }).passthrough()).max(100),
|
|
17
|
+
}).passthrough().nullable(),
|
|
18
|
+
}).passthrough();
|
|
19
|
+
const readiness = z.object({
|
|
20
|
+
generatedAt: z.string().datetime(),
|
|
21
|
+
thresholds: z.object({ staleNodeMs: count }).passthrough(),
|
|
22
|
+
nodes: z.array(node).max(100),
|
|
23
|
+
}).passthrough();
|
|
24
|
+
const output = z.object({
|
|
25
|
+
source: z.string().url(), source_date: z.string().datetime().nullable(), observed_at: z.string().datetime(),
|
|
26
|
+
generated_at: z.string().datetime(), stale_threshold_ms: count, node_count: count, stale_count: count,
|
|
27
|
+
nodes: z.array(z.object({
|
|
28
|
+
node_id: z.string(), name: z.string(), hostname: z.string().nullable(),
|
|
29
|
+
operational_status: z.string(), heartbeat_age_ms: count.nullable(), stale: z.boolean(),
|
|
30
|
+
health_snapshot_at: z.string().datetime().nullable(), cpu_usage_percent: z.number().nullable(),
|
|
31
|
+
memory_percent: z.number().nullable(), disk_warning_count: count,
|
|
32
|
+
}).strict()).max(100),
|
|
33
|
+
}).strict();
|
|
34
|
+
function binding(env) {
|
|
35
|
+
const base = env.ARDENT_MAGNUS_API_URL;
|
|
36
|
+
const token = env.ARDENT_MAGNUS_READ_TOKEN;
|
|
37
|
+
if (!base || !token)
|
|
38
|
+
throw Object.assign(new Error('Magnus read-only service binding is unavailable'), { code: 'SERVICE_BINDING_UNAVAILABLE' });
|
|
39
|
+
let url;
|
|
40
|
+
try {
|
|
41
|
+
url = new URL(base);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw Object.assign(new Error('Invalid Magnus service origin'), { code: 'INVALID_SERVICE_BINDING' });
|
|
45
|
+
}
|
|
46
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/' ||
|
|
47
|
+
(url.protocol === 'http:' && !['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))
|
|
48
|
+
throw Object.assign(new Error('Magnus bearer token requires an HTTPS origin outside loopback'), { code: 'INVALID_SERVICE_BINDING' });
|
|
49
|
+
url.pathname = '/api/v1/ops/readiness';
|
|
50
|
+
return { url, token };
|
|
51
|
+
}
|
|
52
|
+
async function read(env, signal) {
|
|
53
|
+
const { url, token } = binding(env);
|
|
54
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
55
|
+
signal, redirect: 'manual' });
|
|
56
|
+
if (!response.ok)
|
|
57
|
+
throw Object.assign(new Error(`Magnus readiness returned HTTP ${response.status}`), { code: 'SERVICE_READ_FAILED' });
|
|
58
|
+
if (Number(response.headers.get('content-length') ?? 0) > 512_000)
|
|
59
|
+
throw Object.assign(new Error('Magnus readiness response exceeds limit'), { code: 'SERVICE_RESPONSE_TOO_LARGE' });
|
|
60
|
+
if (!response.body)
|
|
61
|
+
throw Object.assign(new Error('Magnus readiness response has no body'), { code: 'SERVICE_READ_FAILED' });
|
|
62
|
+
const reader = response.body.getReader();
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let size = 0;
|
|
65
|
+
try {
|
|
66
|
+
for (;;) {
|
|
67
|
+
const part = await reader.read();
|
|
68
|
+
if (part.done)
|
|
69
|
+
break;
|
|
70
|
+
size += part.value.byteLength;
|
|
71
|
+
if (size > 512_000) {
|
|
72
|
+
await reader.cancel();
|
|
73
|
+
throw Object.assign(new Error('Magnus readiness response exceeds limit'), { code: 'SERVICE_RESPONSE_TOO_LARGE' });
|
|
74
|
+
}
|
|
75
|
+
chunks.push(Buffer.from(part.value));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
reader.releaseLock();
|
|
80
|
+
}
|
|
81
|
+
const parsed = z.object({ success: z.literal(true), data: readiness }).passthrough()
|
|
82
|
+
.parse(JSON.parse(Buffer.concat(chunks, size).toString('utf8'))).data;
|
|
83
|
+
const observed = Date.now();
|
|
84
|
+
const generated = Date.parse(parsed.generatedAt);
|
|
85
|
+
if (!Number.isFinite(generated) || generated < observed - 60_000 || generated > observed + 5_000)
|
|
86
|
+
throw Object.assign(new Error('Magnus readiness observation is stale or future dated'), { code: 'SOURCE_OBSERVATION_STALE' });
|
|
87
|
+
const ids = new Set(parsed.nodes.map(item => item.id));
|
|
88
|
+
if (ids.size !== parsed.nodes.length)
|
|
89
|
+
throw Object.assign(new Error('Magnus readiness contains duplicate node IDs'), { code: 'SOURCE_IDENTITY_MISMATCH' });
|
|
90
|
+
const date = response.headers.get('date');
|
|
91
|
+
// These records represent service audit actors, not heartbeat-producing hosts.
|
|
92
|
+
// Match only the two reserved IDs; real offline nodes remain in the projection.
|
|
93
|
+
const hosts = parsed.nodes.filter(item => !['__control-api__', '__dashboard__'].includes(item.id));
|
|
94
|
+
return output.parse({ source: url.toString(), source_date: date && Number.isFinite(Date.parse(date)) ? new Date(date).toISOString() : null,
|
|
95
|
+
observed_at: new Date(observed).toISOString(), generated_at: parsed.generatedAt,
|
|
96
|
+
stale_threshold_ms: parsed.thresholds.staleNodeMs, node_count: hosts.length,
|
|
97
|
+
stale_count: hosts.filter(item => item.stale).length,
|
|
98
|
+
nodes: hosts.map(item => ({ node_id: item.id, name: item.name, hostname: item.hostname,
|
|
99
|
+
operational_status: item.operationalStatus, heartbeat_age_ms: item.heartbeatAgeMs, stale: item.stale,
|
|
100
|
+
health_snapshot_at: item.health?.timestamp ?? null, cpu_usage_percent: item.health?.cpuUsagePercent ?? null,
|
|
101
|
+
memory_percent: item.health?.memoryPercent ?? null, disk_warning_count: item.health?.diskWarnings.length ?? 0 })) });
|
|
102
|
+
}
|
|
103
|
+
export function magnusFleetReadinessTool(env) {
|
|
104
|
+
return defineTool({ name: 'magnus_fleet_readiness',
|
|
105
|
+
description: 'Read the owning Control API fleet readiness, stale heartbeat posture, and bounded health timestamps. No deployment or remediation authority.',
|
|
106
|
+
input: z.object({}).strict(), output, effect: 'read', contexts: ['direct', 'peer'], timeoutMs: 15_000,
|
|
107
|
+
handler: async (_input, signal) => read(env, signal) });
|
|
108
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ArdentRuntime } from './runtime.js';
|
|
2
|
+
import type { JobEngine } from './job-engine.js';
|
|
3
|
+
type MattermostConfig = ArdentRuntime['config']['channels']['mattermost'];
|
|
4
|
+
export declare class MattermostAdapter {
|
|
5
|
+
#private;
|
|
6
|
+
readonly runtime: ArdentRuntime;
|
|
7
|
+
readonly config: MattermostConfig;
|
|
8
|
+
constructor(runtime: ArdentRuntime, config: MattermostConfig, env?: NodeJS.ProcessEnv, jobs?: JobEngine);
|
|
9
|
+
initialize(): Promise<{
|
|
10
|
+
botId: string;
|
|
11
|
+
username: string;
|
|
12
|
+
}>;
|
|
13
|
+
handlePost(raw: unknown, channelType: string, signal?: AbortSignal, freshOnly?: boolean): Promise<void>;
|
|
14
|
+
reconcile(freshOnly?: boolean, signal?: AbortSignal): Promise<number>;
|
|
15
|
+
connectOnce(signal: AbortSignal): Promise<void>;
|
|
16
|
+
run(signal: AbortSignal): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|