diff --git a/bin/hellgraph-agent-ingest.mjs b/bin/hellgraph-agent-ingest.mjs new file mode 100755 index 0000000..12d921a --- /dev/null +++ b/bin/hellgraph-agent-ingest.mjs @@ -0,0 +1,13 @@ +#!/usr/bin/env node +// Ingest an agent.v1 KnowledgeUpdate (from netwatch et al.) into the HellGraph +// AtomSpace. Reads JSON from a file arg or stdin. Usage: +// hellgraph-agent-ingest system_graph.json +// turtle-netwatch graph --json | hellgraph-agent-ingest - +import { readFileSync } from 'node:fs' +import { ingestKnowledgeUpdate } from '../ts/dist/index.mjs' + +const arg = process.argv[2] +const raw = arg && arg !== '-' ? readFileSync(arg, 'utf8') : readFileSync(0, 'utf8') +const doc = JSON.parse(raw) +const res = ingestKnowledgeUpdate(doc) +console.log(JSON.stringify({ ingested: res })) diff --git a/package.json b/package.json index 6e4b139..51ca6c6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "typescript": "^5.5.0" }, "bin": { - "hellgraph-superpeer": "bin/hellgraph-superpeer.mjs" + "hellgraph-superpeer": "bin/hellgraph-superpeer.mjs", + "hellgraph-agent-ingest": "bin/hellgraph-agent-ingest.mjs" } } diff --git a/ts/dist/index.d.mts b/ts/dist/index.d.mts index 066632d..3daa451 100644 Binary files a/ts/dist/index.d.mts and b/ts/dist/index.d.mts differ diff --git a/ts/dist/index.d.ts b/ts/dist/index.d.ts index 066632d..3daa451 100644 Binary files a/ts/dist/index.d.ts and b/ts/dist/index.d.ts differ diff --git a/ts/dist/index.js b/ts/dist/index.js index 9c827de..e258c09 100644 Binary files a/ts/dist/index.js and b/ts/dist/index.js differ diff --git a/ts/dist/index.mjs b/ts/dist/index.mjs index cc867d9..c6fb5c2 100644 Binary files a/ts/dist/index.mjs and b/ts/dist/index.mjs differ diff --git a/ts/src/agent-graph-ingest.test.ts b/ts/src/agent-graph-ingest.test.ts new file mode 100644 index 0000000..bf1dc71 --- /dev/null +++ b/ts/src/agent-graph-ingest.test.ts @@ -0,0 +1,52 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { AtomSpace } from './atomspace.js' +import { HellGraphStore } from './store.js' +import { ingestKnowledgeUpdate, type KnowledgeUpdate } from './agent-graph-ingest.js' + +function sampleUpdate(): KnowledgeUpdate { + return { + schema: 'agent.v1.KnowledgeUpdate', + graph: 'SYSTEM', + ts: '2026-08-02T00:00:00Z', + patch: { + nodes: [ + { id: 'process:curl#100', kind: 'Process', attrs: { pid: '100', name: 'curl' } }, + { id: 'host:93.184.216.34', kind: 'Host', attrs: { external: 'true' } }, + { id: 'port:443/tcp', kind: 'Port', attrs: { proto: 'tcp' } }, + ], + edges: [ + { from: 'process:curl#100', rel: 'CONNECTS_TO', to: 'host:93.184.216.34', via: 'port:443/tcp', severity: 'WARN', ts: '2026-08-02T00:00:00Z' }, + ], + }, + prov: { source: 'netwatch' }, + } +} + +test('ingests a KnowledgeUpdate into the expected nodes and edges', () => { + const g = new HellGraphStore(new AtomSpace('test-agent-ingest', false)) + const res = ingestKnowledgeUpdate(sampleUpdate(), g) + + assert.equal(res.graph, 'SYSTEM') + assert.equal(res.nodes, 3) + assert.equal(res.edges, 1) + + const ids = new Set(g.allNodes().map((n) => n.id)) + assert.ok(ids.has('process:curl#100')) + assert.ok(ids.has('host:93.184.216.34')) + assert.ok(ids.has('port:443/tcp')) + + const edges = g.allEdges() + const e = edges.find((x) => x.from === 'process:curl#100' && x.to === 'host:93.184.216.34') + assert.ok(e, 'CONNECTS_TO edge present') + assert.equal(e!.label, 'CONNECTS_TO') // AtomSpace stores the relation as edge.label (types.ts) +}) + +test('is tolerant of an empty / malformed patch', () => { + const g = new HellGraphStore(new AtomSpace('test-agent-ingest-empty', false)) + const res = ingestKnowledgeUpdate({ patch: {} }, g) + assert.equal(res.nodes, 0) + assert.equal(res.edges, 0) + assert.equal(res.graph, 'SYSTEM') // default graph + assert.equal(g.allNodes().length, 0) +}) diff --git a/ts/src/agent-graph-ingest.ts b/ts/src/agent-graph-ingest.ts new file mode 100644 index 0000000..9b9cfdd --- /dev/null +++ b/ts/src/agent-graph-ingest.ts @@ -0,0 +1,72 @@ +/** + * agent-graph-ingest — ingest an `agent.v1` KnowledgeUpdate delta into the + * HellGraph AtomSpace, so a node agent's observations (e.g. TurtleTerm + * `turtle-netwatch`) become queryable graph memory rather than a delta written + * to a sink nothing consumes. + * + * The KnowledgeUpdate shape is the one emitted by netwatch + * (SourceOS-Linux/TurtleTerm assets/sourceos/schemas/agent/knowledge_update.avsc): + * { graph: 'SYSTEM'|'USER', ts, patch: { nodes:[{id,kind,attrs}], + * edges:[{from,rel,to,via,severity,ts}] }, prov } + * + * Each node -> g.addNode(id, [kind, `${graph}Graph`], {...attrs, graph}); each + * edge -> g.addEdge(rel, from, to, {via, severity, ts, graph}). Mirrors the + * write path in acr.ts (the supported façade encoding). Ingestion is memory, + * not a security mutation — it does not gate or refuse. + */ +import { getHellGraph } from './store.js' + +type Store = ReturnType + +export interface KnowledgeUpdateNode { + id: string + kind: string + attrs?: Record +} + +export interface KnowledgeUpdateEdge { + from: string + rel: string + to: string + via?: string + severity?: string + ts?: string +} + +export interface KnowledgeUpdate { + schema?: string + graph?: string // 'SYSTEM' | 'USER' + ts?: string + patch: { nodes?: KnowledgeUpdateNode[]; edges?: KnowledgeUpdateEdge[] } + prov?: Record +} + +export interface IngestResult { + graph: string + nodes: number + edges: number +} + +/** Ingest one KnowledgeUpdate. Defaults to the process-wide HellGraph singleton + * (persistent SYSTEM graph); pass a store for tests or a scoped graph. */ +export function ingestKnowledgeUpdate(doc: KnowledgeUpdate, g: Store = getHellGraph()): IngestResult { + const graph = doc.graph ?? 'SYSTEM' + const patch = doc.patch ?? {} + const nodes = patch.nodes ?? [] + const edges = patch.edges ?? [] + + for (const n of nodes) { + if (!n || !n.id || !n.kind) continue + g.addNode(n.id, [n.kind, `${graph}Graph`], { ...(n.attrs ?? {}), graph }) + } + for (const e of edges) { + if (!e || !e.from || !e.to || !e.rel) continue + g.addEdge(e.rel, e.from, e.to, { + via: e.via ?? null, + severity: e.severity ?? 'INFO', + ts: e.ts ?? doc.ts ?? null, + graph, + }) + } + return { graph, nodes: nodes.length, edges: edges.length } +} diff --git a/ts/src/index.ts b/ts/src/index.ts index cc5341a..388e1c8 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -76,3 +76,4 @@ export * from './semantic-action-data' export * from './nlq' export * from './effect-request-data' export * from './vendor-graph' +export * from './agent-graph-ingest'