Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bin/hellgraph-agent-ingest.mjs
Original file line number Diff line number Diff line change
@@ -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 }))
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Binary file modified ts/dist/index.d.mts
Binary file not shown.
Binary file modified ts/dist/index.d.ts
Binary file not shown.
Binary file modified ts/dist/index.js
Binary file not shown.
Binary file modified ts/dist/index.mjs
Binary file not shown.
52 changes: 52 additions & 0 deletions ts/src/agent-graph-ingest.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
72 changes: 72 additions & 0 deletions ts/src/agent-graph-ingest.ts
Original file line number Diff line number Diff line change
@@ -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<typeof getHellGraph>

export interface KnowledgeUpdateNode {
id: string
kind: string
attrs?: Record<string, unknown>
}

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<string, unknown>
}

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 }
}
1 change: 1 addition & 0 deletions ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading