From 463b84d1583dd43adbac52dd4195f43d44526e8e Mon Sep 17 00:00:00 2001 From: Michael Heller Date: Sun, 2 Aug 2026 22:43:46 -0400 Subject: [PATCH] feat(ingest): agent.v1 KnowledgeUpdate -> AtomSpace (System Graph memory) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the graph-memory half of consent-plane loop #2: a node agent's observations (e.g. TurtleTerm turtle-netwatch) become queryable HellGraph memory instead of a delta written to a sink nothing consumes. - ts/src/agent-graph-ingest.ts: ingestKnowledgeUpdate(doc, g=getHellGraph()) reads the netwatch KnowledgeUpdate (patch.nodes[{id,kind,attrs}] + patch.edges[{from,rel,to,via,severity,ts}]) and writes via the supported façade path: addNode(id, [kind, `${graph}Graph`], {...attrs, graph}) / addEdge(rel, from, to, {via, severity, ts, graph}). Mirrors acr.ts. - bin/hellgraph-agent-ingest: reads a KnowledgeUpdate from file arg or stdin, ingests into the process HellGraph singleton, prints {ingested:{nodes,edges}}. - exported from index; rebuilt dist. - test: a sample KnowledgeUpdate produces the expected Process/Host/Port nodes + CONNECTS_TO edge (relation stored as edge.label); tolerant of empty patch. Ingestion is memory, not a security mutation — it does not gate/refuse (fail-soft), unlike the consent/Governor gates. The netwatch `graph --ingest` wire follows in TurtleTerm. --- bin/hellgraph-agent-ingest.mjs | 13 ++++++ package.json | 3 +- ts/dist/index.d.mts | Bin 236309 -> 238108 bytes ts/dist/index.d.ts | Bin 236309 -> 238108 bytes ts/dist/index.js | Bin 432473 -> 433227 bytes ts/dist/index.mjs | Bin 423479 -> 424201 bytes ts/src/agent-graph-ingest.test.ts | 52 +++++++++++++++++++++ ts/src/agent-graph-ingest.ts | 72 ++++++++++++++++++++++++++++++ ts/src/index.ts | 1 + 9 files changed, 140 insertions(+), 1 deletion(-) create mode 100755 bin/hellgraph-agent-ingest.mjs create mode 100644 ts/src/agent-graph-ingest.test.ts create mode 100644 ts/src/agent-graph-ingest.ts 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 066632d450759de812ce24d2d739e774b2d56cc5..3daa451284d7bb89a8520561725206cdd3aaf889 100644 GIT binary patch delta 1595 zcmZux&uEQJqPMcrgcT40rluPxk%iqT4Nx0+yZ-`?K1B*68{tlGj^L+M?#1W__$c#* zLGUp)f^r2gc~h1mTw))l7_I1PdAxOr)BD5(GgJ4uUL>knO3+7)&`I zK@^~)beDjbkh&|tquP0b+NFCA8HRYOLmi3?A=2C*$P@t+PU*vV2FK9&ZF?Yd<>AkR z?)@muf9-u}287mVbWhs=NgrN_i6H(huf5%4PM_qKR98&3*XeUofO*wpA6kRQgW<+z z>$ldU!Nyk00izwrn3z5VF}J$EJ&$~6H%fg+7^C!J$ElJ4uw;BcQ8IB9#*UFrF^z5t%1Xi6SX^WdLUJGo zPq~Jy#5(Cb%cNqJCgQ(81^EtBPx>S}Ww~cP9?2qQSu68cqR1C)5%h9q1d`lXdsfq6 zz?Z1mI}ot*r2>VMTqB#O*KC>@*<@g(Liho;kj$G7>F9etmlFukoJ_{rYLg}gzfcnz zj=JH><<^yh(etHmzggI*j|MSw2XDMHUzUUMa{0RT`B|cEy$A6)(V8k1GgP)WCiI*$!hJy^I0@18E*C>BlHXh-teQLTY-;TBE=JtYU(eZ^+? zQ0wkqL>4*Ghqpu!qmg!Rv@=vX(zHbkR5MulnrxlXWV2(e>XWa?5{9-XEjPudp3KOA z7GL@A=u-LS|4-Ixh}ypNPH{$aLj+kXb^1r`R4pHF>c-OfPkBY#gM(W|0NMYX#156V4uFIKgaWh!P;Hq)$ diff --git a/ts/dist/index.d.ts b/ts/dist/index.d.ts index 066632d450759de812ce24d2d739e774b2d56cc5..3daa451284d7bb89a8520561725206cdd3aaf889 100644 GIT binary patch delta 1595 zcmZux&uEQJqPMcrgcT40rluPxk%iqT4Nx0+yZ-`?K1B*68{tlGj^L+M?#1W__$c#* zLGUp)f^r2gc~h1mTw))l7_I1PdAxOr)BD5(GgJ4uUL>knO3+7)&`I zK@^~)beDjbkh&|tquP0b+NFCA8HRYOLmi3?A=2C*$P@t+PU*vV2FK9&ZF?Yd<>AkR z?)@muf9-u}287mVbWhs=NgrN_i6H(huf5%4PM_qKR98&3*XeUofO*wpA6kRQgW<+z z>$ldU!Nyk00izwrn3z5VF}J$EJ&$~6H%fg+7^C!J$ElJ4uw;BcQ8IB9#*UFrF^z5t%1Xi6SX^WdLUJGo zPq~Jy#5(Cb%cNqJCgQ(81^EtBPx>S}Ww~cP9?2qQSu68cqR1C)5%h9q1d`lXdsfq6 zz?Z1mI}ot*r2>VMTqB#O*KC>@*<@g(Liho;kj$G7>F9etmlFukoJ_{rYLg}gzfcnz zj=JH><<^yh(etHmzggI*j|MSw2XDMHUzUUMa{0RT`B|cEy$A6)(V8k1GgP)WCiI*$!hJy^I0@18E*C>BlHXh-teQLTY-;TBE=JtYU(eZ^+? zQ0wkqL>4*Ghqpu!qmg!Rv@=vX(zHbkR5MulnrxlXWV2(e>XWa?5{9-XEjPudp3KOA z7GL@A=u-LS|4-Ixh}ypNPH{$aLj+kXb^1r`R4pHF>c-OfPkBY#gM(W|0NMYX#156V4uFIKgaWh!P;Hq)$ diff --git a/ts/dist/index.js b/ts/dist/index.js index 9c827de43c2a0a4949660776e386db77ce3aef80..e258c0931f3c78a44470bda95d2005e3686a3681 100644 GIT binary patch delta 624 zcmZuv%SyvQ6ve(M;;z<3EOJ{4wvbNuigcwSVnwP8kwQZ=O@nc!Bx6O=P{Ftty2xiJ zBGiBI3v}T^@CWmt9x#@ zZdjPGn&ldUe$6Hp_E>ZqCYLpZcL}|4uxa6=foU*YF=sa>NHy=RkU z&F(>^NMJOAB0;tpgHxNB(cNnzKW_4V6_L^5gdh>up;Pvy#9&)E?zDCA0U<F7M2#)Eo>a=+Ye{5<;!i)>|lE&3;^ME45$DA diff --git a/ts/dist/index.mjs b/ts/dist/index.mjs index cc867d96aa91711f589233b73e26b3e4e02ea5e0..c6fb5c29030af812e4abbc3a3c0038ff7e5f3f3e 100644 GIT binary patch delta 651 zcmZ9Kze~eF6vr`|3ZfMe6n_lQ;E+OkLEIF{q#~jY)}a)kX>v`2@lujgkr?Wb#mT`T ze}S8$(8Wdn2}i-%MgIctN3`N_yzkxn6MI(y$N_4)XG=GT+09NdqLsBMq4W;TYiDx7XMa3)}`d3j(9a#P}RXKE34WAI%wU zXMgKJ`=Mye^Hem!$0gO@I;v@eyvq9 O$0z^2n|?w{Pov+gi_bj( delta 33 pcmeDDB)R>KWJ3#M3sVbo3rh>@7PiQw?INjcT0GlB3)!}?0|47V3zGl< 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'