neuralos 3.9.4 → 3.9.6

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 (68) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +48 -347
  3. package/cli.js +78 -0
  4. package/engine.js +124 -0
  5. package/index.js +160 -0
  6. package/package.json +37 -43
  7. package/test.js +20 -0
  8. package/tools.js +47 -0
  9. package/bin/gybackend.cjs +0 -398530
  10. package/plugins/agentspan-bridge/agentspan-bridge.extreme.spec.ts +0 -328
  11. package/plugins/agentspan-bridge/agentspan-bridge.phase2.extreme.spec.ts +0 -282
  12. package/plugins/agentspan-bridge/conductorClient.mjs +0 -182
  13. package/plugins/agentspan-bridge/index.mjs +0 -389
  14. package/plugins/agentspan-bridge/playbookToWorkflowDef.mjs +0 -209
  15. package/plugins/agentspan-bridge/plugin.json +0 -27
  16. package/plugins/fraudops/index.d.mts +0 -9
  17. package/plugins/fraudops/index.mjs +0 -185
  18. package/plugins/fraudops/plugin.json +0 -10
  19. package/plugins/iam-connector/index.d.mts +0 -10
  20. package/plugins/iam-connector/index.mjs +0 -229
  21. package/plugins/iam-connector/plugin.json +0 -10
  22. package/plugins/mitmproxy-bridge/index.d.mts +0 -20
  23. package/plugins/mitmproxy-bridge/index.mjs +0 -256
  24. package/plugins/mitmproxy-bridge/plugin.json +0 -17
  25. package/plugins/monid-bridge/index.mjs +0 -228
  26. package/plugins/monid-bridge/monid-bridge.extreme.spec.mjs +0 -75
  27. package/plugins/monid-bridge/plugin.json +0 -15
  28. package/plugins/netdata-rterm/index.mjs +0 -229
  29. package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +0 -252
  30. package/plugins/netdata-rterm/plugin.json +0 -22
  31. package/plugins/netexec-bridge/index.d.mts +0 -28
  32. package/plugins/netexec-bridge/index.mjs +0 -325
  33. package/plugins/netexec-bridge/plugin.json +0 -20
  34. package/plugins/neuralos/index.mjs +0 -137
  35. package/plugins/neuralos/neuralos.extreme.spec.mts +0 -386
  36. package/plugins/neuralos/neuralosEngine.mjs +0 -369
  37. package/plugins/neuralos/plugin.json +0 -15
  38. package/plugins/numbat-bridge/index.mjs +0 -229
  39. package/plugins/numbat-bridge/numbat-bridge.extreme.spec.mjs +0 -148
  40. package/plugins/numbat-bridge/plugin.json +0 -21
  41. package/plugins/patch-manager/index.d.mts +0 -10
  42. package/plugins/patch-manager/index.mjs +0 -288
  43. package/plugins/patch-manager/plugin.json +0 -10
  44. package/plugins/promptfoo-redteam/index.d.mts +0 -15
  45. package/plugins/promptfoo-redteam/index.mjs +0 -277
  46. package/plugins/promptfoo-redteam/plugin.json +0 -16
  47. package/plugins/reactorpro-bridge/index.mjs +0 -499
  48. package/plugins/reactorpro-bridge/plugin.json +0 -26
  49. package/plugins/reactorpro-bridge/reactorpro-bridge.extreme.spec.mts +0 -409
  50. package/plugins/reactorpro-bridge/reactorproAgent.mjs +0 -399
  51. package/plugins/request-router/index.d.mts +0 -9
  52. package/plugins/request-router/index.mjs +0 -239
  53. package/plugins/request-router/plugin.json +0 -10
  54. package/plugins/sample-k8s-slo/index.ts +0 -58
  55. package/plugins/sample-k8s-slo/plugin.json +0 -11
  56. package/plugins/sop-assistant/index.d.mts +0 -9
  57. package/plugins/sop-assistant/index.mjs +0 -325
  58. package/plugins/sop-assistant/plugin.json +0 -10
  59. package/plugins/synapse-bridge/index.mjs +0 -484
  60. package/plugins/synapse-bridge/plugin.json +0 -29
  61. package/plugins/synapse-bridge/synapse-bridge.extreme.spec.mjs +0 -204
  62. package/plugins/synapse-bridge/synapseAgent.extreme.spec.mjs +0 -236
  63. package/plugins/synapse-bridge/synapseAgent.mjs +0 -254
  64. package/plugins/web-intel/index.mjs +0 -381
  65. package/plugins/web-intel/plugin.json +0 -28
  66. package/plugins/web-intel/sidecar.mjs +0 -134
  67. package/plugins/web-intel/web-intel.extreme.spec.ts +0 -263
  68. package/plugins/web-intel/wigoloClient.mjs +0 -145
@@ -1,369 +0,0 @@
1
- /**
2
- * neuralosEngine — the neuralOS runtime for the RTerm neuralos plugin.
3
- *
4
- * neuralOS instances are on-device data agents: each instance directory
5
- * carries a probe menu (needle_menu.json) over a real data source, the
6
- * 121M engine binary picks the probe that answers a question, and the
7
- * instance's bridge (Python) executes it against the source. Everything
8
- * here returns errors as data — a missing engine or instance is an
9
- * answer, never a thrown exception.
10
- *
11
- * Engine resolution order (first existing wins):
12
- * 1. settings.neuralos.engineBin/engineWeights (or NEURALOS_ENGINE_* env)
13
- * 2. the desktop bundle: {process.resourcesPath}/neuralos/<per-arch engine>
14
- * (electron-builder extraResources; present in the packaged app only)
15
- * 3. the shared cache ~/.cache/neuralos/ (auto-downloaded on first use —
16
- * this is what makes `npm i -g neuralos` self-provisioning: one fetch,
17
- * then offline forever)
18
- * 4. <instancesRoot>/engine/needle (+ needle3.cact) — the fleet convention
19
- * If nothing exists and autoDownload is enabled, the cache is provisioned
20
- * from https://huggingface.co/Cactus-Compute/needle3 (public, no auth).
21
- */
22
-
23
- import { execFile as cpExecFile, spawn } from 'node:child_process'
24
- import { promises as fs, constants as fsConstants } from 'node:fs'
25
- import { homedir } from 'node:os'
26
- import path from 'node:path'
27
-
28
- export const HF_BASE = 'https://huggingface.co/Cactus-Compute/needle3/resolve/main'
29
- export const WEIGHTS_NAME = 'needle3.cact'
30
- export const MAX_RESULT_CHARS = 6000
31
-
32
- /** Per-arch bundled engine name, matching the release-workflow download. */
33
- export function bundledEngineName(platform, arch) {
34
- const os = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux'
35
- if (os === 'macos') return arch === 'arm64' ? 'engine-macos-arm64' : null
36
- if (os === 'linux') return arch === 'arm64' ? 'engine-linux-arm64' : arch === 'x64' ? 'engine-linux-x86_64' : null
37
- return arch === 'arm64' ? 'engine-windows-arm64.exe' : arch === 'x64' ? 'engine-windows-x86_64.exe' : null
38
- }
39
-
40
- /** The HF platform folder for a bundled engine name. */
41
- export function hfFolderFor(platform, arch) {
42
- const os = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux'
43
- if (os === 'macos') return 'macos-arm64'
44
- if (os === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x86_64'
45
- return arch === 'arm64' ? 'windows-arm64' : 'windows-x86_64'
46
- }
47
-
48
- export function cacheDir(env = process.env) {
49
- if (env.NEURALOS_CACHE_DIR) return env.NEURALOS_CACHE_DIR
50
- return path.join(homedir(), '.cache', 'neuralos')
51
- }
52
-
53
- /** Settings block -> env -> default. Blank values mean unset. */
54
- function pick(settingsValue, envValue, fallback) {
55
- const s = typeof settingsValue === 'string' ? settingsValue.trim() : ''
56
- if (s) return s
57
- const e = typeof envValue === 'string' ? envValue.trim() : ''
58
- if (e) return e
59
- return fallback
60
- }
61
-
62
- export function resolveConfig(ctx = {}, env = process.env) {
63
- const settings = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
64
- const block = settings.neuralos || {}
65
- return {
66
- instancesDir: pick(block.instancesDir, env.NEURALOS_INSTANCES_DIR, path.join(homedir(), 'neuralos-instances')),
67
- pythonBin: pick(block.pythonBin, env.NEURALOS_PYTHON, 'python3'),
68
- engineBin: pick(block.engineBin, env.NEURALOS_ENGINE_BIN) || null,
69
- engineWeights: pick(block.engineWeights, env.NEURALOS_ENGINE_WEIGHTS) || null,
70
- autoDownload: block.autoDownload !== false && env.NEURALOS_AUTO_DOWNLOAD !== '0',
71
- cacheDir: pick(block.cacheDir, env.NEURALOS_CACHE_DIR, cacheDir(env)),
72
- timeoutMs: Number(block.timeoutMs) > 0 ? Number(block.timeoutMs) : 120000,
73
- }
74
- }
75
-
76
- /** Default execFile promisified; injectable for the spec. */
77
- export function defaultExec() {
78
- return (cmd, args, opts = {}) =>
79
- new Promise((resolve) => {
80
- const execEnv = opts.env ? { ...process.env, ...opts.env } : process.env
81
- const child = cpExecFile(cmd, args, { timeout: opts.timeoutMs ?? 120000, maxBuffer: 16 * 1024 * 1024, env: execEnv }, (err, stdout, stderr) => {
82
- // err.code: 'EACCES'/'ENOENT'/'ETIMEDOUT' style codes, undefined for
83
- // signal kills (e.g. our own timeout SIGTERM). Preserve the real
84
- // code; a timeout must not masquerade as a generic exit 1.
85
- let code = 0
86
- if (err) {
87
- code = typeof err.code === 'string' || typeof err.code === 'number' ? err.code : err.signal ? `killed (${err.signal})` : 1
88
- }
89
- resolve({ ok: !err, code, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
90
- })
91
- void child
92
- })
93
- }
94
-
95
- async function exists(p) {
96
- try {
97
- await fs.access(p, fsConstants.F_OK)
98
- return true
99
- } catch {
100
- return false
101
- }
102
- }
103
-
104
- /**
105
- * A candidate is USABLE only if the binary is actually EXECUTABLE.
106
- * Existence alone is not enough: electron-builder's extraResources copy and
107
- * curl-downloaded files routinely land with mode 0644 — spawning them dies
108
- * with EACCES ("engine exited EACCES:" with an empty stderr, the classic
109
- * symptom). We probe X_OK; where the path is OURS to manage (the shared
110
- * cache, the fleet-convention dir, an explicit override) we attempt a
111
- * one-shot chmod recovery and re-probe. The app bundle is NOT ours to
112
- * mutate — a 0644 bundled engine is skipped, never chmod'd, so a broken
113
- * bundle falls through to the next runnable engine instead of shadowing it.
114
- */
115
- async function executable(p, { recover = true } = {}) {
116
- try {
117
- await fs.access(p, fsConstants.X_OK)
118
- return true
119
- } catch {
120
- if (!recover) return false
121
- try {
122
- await fs.chmod(p, 0o755)
123
- await fs.access(p, fsConstants.X_OK)
124
- return true
125
- } catch {
126
- return false
127
- }
128
- }
129
- }
130
-
131
- /** Download one file over HTTPS into dir with its basename (idempotent). */
132
- async function download(deps, url, dir) {
133
- await fs.mkdir(dir, { recursive: true })
134
- const target = path.join(dir, path.basename(url))
135
- if (await exists(target)) return target
136
- const tmp = `${target}.part`
137
- if (!deps.fetchImpl) {
138
- const res = await fetch(url, { redirect: 'follow' })
139
- if (!res.ok) throw new Error(`download ${url} -> HTTP ${res.status}`)
140
- const buf = Buffer.from(await res.arrayBuffer())
141
- await fs.writeFile(tmp, buf)
142
- } else {
143
- await deps.fetchImpl(url, tmp)
144
- }
145
- await fs.rename(tmp, target)
146
- return target
147
- }
148
-
149
- /**
150
- * Provision the shared cache with the weights + this platform's engine.
151
- * One-time (~36 MB); afterwards everything resolves offline. Injectable
152
- * fetch/download for the spec; the real path uses global fetch (Node >=18).
153
- */
154
- export async function ensureEngine(deps, { force = false } = {}) {
155
- const engineName = bundledEngineName(deps.platform ?? process.platform, deps.arch ?? process.arch)
156
- if (!engineName) return { error: `no published neuralOS engine for ${deps.platform ?? process.platform}-${deps.arch ?? process.arch}` }
157
- const dir = deps.cacheDir ?? cacheDir()
158
- const weightsPath = path.join(dir, WEIGHTS_NAME)
159
- const enginePath = path.join(dir, engineName)
160
- try {
161
- if (force || !(await exists(weightsPath))) {
162
- await download(deps, `${HF_BASE}/${WEIGHTS_NAME}`, dir)
163
- }
164
- if (force || !(await exists(enginePath))) {
165
- await download(deps, `${HF_BASE}/${hfFolderFor(deps.platform ?? process.platform, deps.arch ?? process.arch)}/${engineName.startsWith('engine-windows') ? 'needle.exe' : 'needle'}`, dir)
166
- // HF keeps upstream names; rename to the per-arch bundled name.
167
- const raw = engineName.startsWith('engine-windows') ? 'needle.exe' : 'needle'
168
- const rawPath = path.join(dir, raw)
169
- if (rawPath !== enginePath && (await exists(rawPath))) await fs.rename(rawPath, enginePath)
170
- }
171
- await fs.chmod(enginePath, 0o755).catch(() => {})
172
- return { engineBin: enginePath, engineWeights: weightsPath }
173
- } catch (e) {
174
- return { error: `engine auto-download failed: ${String(e?.message ?? e)}` }
175
- }
176
- }
177
-
178
- export async function resolveEngine(cfg, deps = {}) {
179
- const platform = deps.platform ?? process.platform
180
- const arch = deps.arch ?? process.arch
181
- const candidates = []
182
- if (cfg.engineBin && cfg.engineWeights) candidates.push({ bin: cfg.engineBin, weights: cfg.engineWeights })
183
- const resources = (deps.resourcesPath ?? process.resourcesPath) ?? undefined
184
- const bundled = bundledEngineName(platform, arch)
185
- if (resources && bundled) {
186
- // the app bundle is not ours to chmod — probe, skip, never recover
187
- candidates.push({ bin: path.join(resources, 'neuralos', bundled), weights: path.join(resources, 'neuralos', WEIGHTS_NAME), recover: false })
188
- }
189
- if (bundled) {
190
- candidates.push({ bin: path.join(cfg.cacheDir, bundled), weights: path.join(cfg.cacheDir, WEIGHTS_NAME) })
191
- }
192
- candidates.push({ bin: path.join(cfg.instancesDir, 'engine', 'needle'), weights: path.join(cfg.instancesDir, 'engine', WEIGHTS_NAME) })
193
- const skipped = []
194
- for (const c of candidates) {
195
- if (!(await exists(c.bin)) || !(await exists(c.weights))) continue
196
- if (!(await executable(c.bin, { recover: c.recover !== false }))) {
197
- // A present-but-unexecutable engine is a broken candidate, never a
198
- // hard stop: record it and fall through to the next candidate.
199
- skipped.push(c.bin)
200
- continue
201
- }
202
- return { engineBin: c.bin, engineWeights: c.weights }
203
- }
204
- if (cfg.autoDownload) {
205
- const ensured = await ensureEngine({ ...deps, cacheDir: cfg.cacheDir, platform, arch })
206
- if (!ensured.error) return ensured
207
- // autoDownload could not provision a usable engine either — surface
208
- // BOTH failures so the operator can see why every path was exhausted.
209
- return {
210
- error: `neuralOS engine not usable: ${ensured.error}${skipped.length ? `; also found but not executable: ${skipped.join(', ')} (chmod +x them or set neuralos.engineBin/engineWeights)` : ''}`,
211
- }
212
- }
213
- return {
214
- error: `neuralOS engine not found or not executable${skipped.length ? ` (found but not executable: ${skipped.join(', ')} — chmod +x them or set neuralos.engineBin/engineWeights)` : ' — set neuralos.engineBin/engineWeights (settings), install the desktop bundle, or enable autoDownload'}`,
215
- }
216
- }
217
-
218
- export function parseJsonObject(text) {
219
- try {
220
- return JSON.parse(text)
221
- } catch {
222
- const first = text.indexOf('{')
223
- const last = text.lastIndexOf('}')
224
- if (first === -1 || last <= first) return null
225
- try {
226
- return JSON.parse(text.slice(first, last + 1))
227
- } catch {
228
- return null
229
- }
230
- }
231
- }
232
-
233
- export async function listInstances(cfg, deps = {}) {
234
- const readdir = deps.readdir ?? fs.readdir
235
- const readFile = deps.readFile ?? fs.readFile
236
- let entries
237
- try {
238
- entries = await readdir(cfg.instancesDir, { withFileTypes: true })
239
- } catch {
240
- return { error: `no instances directory at ${cfg.instancesDir} — set neuralos.instancesDir (settings) or NEURALOS_INSTANCES_DIR` }
241
- }
242
- const instances = []
243
- for (const entry of entries) {
244
- if (!entry.isDirectory()) continue
245
- const dir = path.join(cfg.instancesDir, entry.name)
246
- try {
247
- const raw = await readFile(path.join(dir, 'needle_menu.json'), 'utf-8')
248
- const menu = JSON.parse(raw)
249
- if (!Array.isArray(menu)) continue
250
- instances.push({ name: entry.name, probeCount: menu.length, probes: menu.slice(0, 40).map((t) => t.name) })
251
- } catch {
252
- continue
253
- }
254
- }
255
- return instances
256
- }
257
-
258
- export async function instanceDirFor(cfg, instance) {
259
- if (!/^[A-Za-z0-9._-]+$/.test(instance)) return { error: `invalid instance name: ${instance}` }
260
- const dir = path.join(cfg.instancesDir, instance)
261
- if (!(await exists(path.join(dir, 'needle_menu.json')))) {
262
- return { error: `no instance named '${instance}' (see neuralos_list_instances)` }
263
- }
264
- return dir
265
- }
266
-
267
- export async function engineSelect(exec, engine, menuPath, question, opts = {}) {
268
- let out
269
- try {
270
- out = await exec(engine.engineBin, ['--model', engine.engineWeights, '--tools', menuPath, '--prompt', question], { timeoutMs: opts.timeoutMs })
271
- } catch (e) {
272
- return { error: `engine execution failed: ${String(e?.message ?? e)}` }
273
- }
274
- if (out.code !== 0 && !out.stdout.trim()) {
275
- // EACCES on spawn surfaces as code 'EACCES' with EMPTY stderr — the
276
- // classic lost-exec-bit symptom. Name the fix instead of a bare code.
277
- if (out.code === 'EACCES' || /EACCES|permission denied/i.test(String(out.stderr))) {
278
- return {
279
- error: `engine not executable (${engine.engineBin}) — restore the exec bit (chmod +x) or set neuralos.engineBin/engineWeights to a runnable binary`,
280
- }
281
- }
282
- return { error: `engine exited ${out.code}: ${out.stderr.slice(0, 200)}` }
283
- }
284
- const parsed = parseJsonObject(out.stdout)
285
- if (!parsed) return { error: `engine output not JSON: ${out.stdout.slice(0, 120)}` }
286
- const calls = parsed.function_calls
287
- if (!Array.isArray(calls) || calls.length === 0 || !calls[0]?.name) {
288
- return { error: 'engine refused the question (no call selected)' }
289
- }
290
- return { pick: calls[0].name, args: calls[0].arguments ?? {}, confidence: typeof parsed.confidence === 'number' ? parsed.confidence : null }
291
- }
292
-
293
- const PROBE_RUNNER = [
294
- 'import json, os, sys',
295
- 'sys.path.insert(0, os.environ["NEURALOS_INSTANCE_DIR"])',
296
- 'import bridge',
297
- 'fn = getattr(bridge, os.environ["NEURALOS_PROBE"], None)',
298
- 'if fn is None:',
299
- ' print(json.dumps({"error": "unknown probe", "probe": os.environ["NEURALOS_PROBE"]}))',
300
- 'else:',
301
- ' print(json.dumps(fn(**json.loads(os.environ.get("NEURALOS_ARGS", "{}"))), default=str))',
302
- ].join('\n')
303
-
304
- export async function executeProbe(exec, cfg, instanceDir, probe, args, opts = {}) {
305
- const env = {
306
- NEURALOS_INSTANCE_DIR: instanceDir,
307
- NEURALOS_PROBE: probe,
308
- NEURALOS_ARGS: JSON.stringify(args ?? {}),
309
- }
310
- let out
311
- try {
312
- out = await exec(cfg.pythonBin, ['-c', PROBE_RUNNER], { ...opts, env })
313
- } catch (e) {
314
- return { error: `probe execution failed: ${String(e?.message ?? e)}` }
315
- }
316
- if (out.code !== 0 && !out.stdout.trim()) return { error: `probe exited ${out.code}: ${out.stderr.slice(0, 200)}` }
317
- const parsed = parseJsonObject(out.stdout)
318
- if (!parsed || typeof parsed !== 'object') return { error: `probe output not JSON: ${out.stdout.slice(0, 120)}` }
319
- return parsed
320
- }
321
-
322
- /**
323
- * Graph probes execute DIRECTLY by name (bypassing engine selection): probe
324
- * names follow the instance convention <prefix>_graph_<op>, and the 121M
325
- * model grounds numeric fragments poorly — direct execution is the fix.
326
- */
327
- export async function graphProbe(exec, cfg, instanceDir, input) {
328
- if (!['overview', 'neighbors', 'connect'].includes(input.op)) return { error: `unknown graph op: ${input.op}` }
329
- // A corrupt/unreadable menu is an answer, never a thrown exception —
330
- // every other read path in this file already reports errors as data.
331
- let menu
332
- try {
333
- const raw = await fs.readFile(path.join(instanceDir, 'needle_menu.json'), 'utf-8')
334
- menu = JSON.parse(raw)
335
- if (!Array.isArray(menu)) throw new Error('menu is not an array')
336
- } catch (e) {
337
- return { error: `instance menu unreadable: ${String(e?.message ?? e)}` }
338
- }
339
- const graphNames = menu.map((t) => t.name).filter((n) => n.includes('_graph_'))
340
- const probe = graphNames.find((n) => n.endsWith(`_graph_${input.op}`))
341
- if (!probe) return { error: `this instance has no ${input.op} graph probe`, available_graph_probes: graphNames }
342
- const args = input.op === 'neighbors' ? { node: input.node ?? '' } : input.op === 'connect' ? { a: input.a ?? '', b: input.b ?? '' } : {}
343
- const result = await executeProbe(exec, cfg, instanceDir, probe, args)
344
- return { probe, ...result }
345
- }
346
-
347
- /**
348
- * Admin probes are write-capable bridge functions; the confirm='yes' literal
349
- * is the operator interlock (enforced at the tool schema) and this guard
350
- * keeps probe names to plain identifiers.
351
- */
352
- export async function adminProbe(exec, cfg, instanceDir, probe, args) {
353
- if (!/^[A-Za-z0-9_]+$/.test(probe)) return { error: `invalid probe name: ${probe}` }
354
- const result = await executeProbe(exec, cfg, instanceDir, probe, args ?? {})
355
- return { probe, admin: true, result }
356
- }
357
-
358
- export function truncate(value, max = MAX_RESULT_CHARS) {
359
- // JSON.stringify(undefined) is undefined (not a string) — a probe that
360
- // returned nothing must not crash the tool; render it honestly instead.
361
- const text = typeof value === 'string' ? value : JSON.stringify(value) ?? 'null'
362
- if (text.length <= max) return text
363
- return `${text.slice(0, max)}\n… (truncated at ${max} chars)`
364
- }
365
-
366
- /** Streamed engine invocation for long-running selections (unused by default). */
367
- export function engineSelectStreamed(engine, menuPath, question) {
368
- return spawn(engine.engineBin, ['--model', engine.engineWeights, '--tools', menuPath, '--prompt', question], { stdio: ['ignore', 'pipe', 'pipe'] })
369
- }
@@ -1,15 +0,0 @@
1
- {
2
- "name": "neuralos",
3
- "version": "1.0.0",
4
- "description": "neuralOS instances as RTerm agent tools — on-device data agents over verified probe menus (chinook, cyberbank, aws, efraud, ...). The 121M neuralOS engine selects the probe for a plain-English question and the instance's Python bridge executes it against the real data source; the engine ships with the desktop bundle and the standalone backend auto-provisions ~/.cache/neuralos on first use. Four tools: neuralos_list_instances, neuralos_ask, neuralos_graph (relationship maps), neuralos_admin (write probes behind a confirm=\"yes\" interlock). Config in Settings (neuralos block: instancesDir, pythonBin, engineBin/engineWeights, autoDownload); instances root defaults to ~/neuralos-instances.",
5
- "entry": "index.mjs",
6
- "tools": [
7
- "neuralos_list_instances",
8
- "neuralos_ask",
9
- "neuralos_graph",
10
- "neuralos_admin"
11
- ],
12
- "permissions": [
13
- "exec"
14
- ]
15
- }
@@ -1,229 +0,0 @@
1
- /**
2
- * numbat-bridge — RTerm ↔ Numbat (endpoint AI-agent detection) integration.
3
- *
4
- * Numbat = EDR for AI agents: endpoint visibility, CEL detection, forensic
5
- * reconstruction. RTerm = control plane. This bridge wires them:
6
- * - DEPLOY: install/manage the numbat binary + hooks on hosts (via playbooks/exec).
7
- * - INGEST: accept Numbat findings (NDJSON records) delivered over HTTP or read
8
- * from a local records file, normalize them, and feed RTerm triggers.
9
- * - ACT: turn detections into governed actions (playbooks, MOP changes, incidents).
10
- *
11
- * Numbat detects; RTerm responds.
12
- *
13
- * Config (settings.numbat, or env):
14
- * enabled — master switch (default true)
15
- * binaryPath — path to the numbat binary (default "numbat" on PATH)
16
- * recordsPath — local NDJSON records file to tail (default ~/.numbat/records.ndjson)
17
- * ingestToken — bearer token the HTTP ingest endpoint requires (vault secretRef ok)
18
- * minSeverity — only ingest findings at/above this severity (info|low|medium|high|critical)
19
- */
20
-
21
- import { createRequire } from 'node:module'
22
- import { randomUUID } from 'node:crypto'
23
-
24
- const require = createRequire(import.meta.url)
25
-
26
- // ─── config ─────────────────────────────────────────────────────────────────
27
-
28
- export function resolveConfig(ctx = {}, env = process.env) {
29
- const s = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
30
- const b = s.numbat || {}
31
- return {
32
- enabled: b.enabled !== false,
33
- binaryPath: b.binaryPath || env.NUMBAT_BIN || 'numbat',
34
- recordsPath: b.recordsPath || env.NUMBAT_RECORDS || `${process.env.HOME}/.numbat/records.ndjson`,
35
- ingestToken: b.ingestToken || undefined,
36
- minSeverity: b.minSeverity || 'low',
37
- }
38
- }
39
-
40
- const SEVERITY_ORDER = ['info', 'low', 'medium', 'high', 'critical']
41
- function severityAtLeast(sev, min) {
42
- const i = SEVERITY_ORDER.indexOf(String(sev ?? 'info').toLowerCase())
43
- const m = SEVERITY_ORDER.indexOf(String(min ?? 'low').toLowerCase())
44
- return (i < 0 ? 0 : i) >= (m < 0 ? 0 : m)
45
- }
46
-
47
- // ─── record normalization ───────────────────────────────────────────────────
48
-
49
- /** Normalize a Numbat NDJSON record (event/finding/enforcement/indicator/scan) into
50
- * a compact RTerm finding. Returns null for records that shouldn't be ingested. */
51
- export function normalizeRecord(rec, cfg) {
52
- if (!rec || typeof rec !== 'object') return null
53
- const type = rec.record_type ?? rec.type ?? 'event'
54
- const severity = rec.severity ?? rec.rule_severity ?? rec.level ?? 'info'
55
- // Only findings + high-signal events become trigger inputs; raw events are noise.
56
- const isFinding = type === 'finding' || type === 'enforcement' || type === 'indicator'
57
- if (!isFinding && type === 'event' && !severityAtLeast(severity, 'high')) return null
58
- if (!severityAtLeast(severity, cfg.minSeverity)) return null
59
- return {
60
- id: rec.id ?? rec.record_id ?? randomUUID(),
61
- source: 'numbat',
62
- recordType: type,
63
- severity,
64
- ruleId: rec.rule_id ?? rec.rule?.id ?? undefined,
65
- title: rec.title ?? rec.rule_name ?? rec.rule?.name ?? type,
66
- agent: rec.agent ?? rec.agent_id ?? rec.source?.agent ?? undefined,
67
- host: rec.host ?? rec.hostname ?? rec.source?.host ?? undefined,
68
- summary: rec.summary ?? rec.description ?? rec.content_preview ?? undefined,
69
- ts: rec.ts ?? rec.timestamp ?? new Date().toISOString(),
70
- raw: rec,
71
- }
72
- }
73
-
74
- /** Parse an NDJSON blob (one JSON record per line) into normalized findings. */
75
- export function parseNdjson(text, cfg) {
76
- const out = []
77
- for (const line of String(text ?? '').split(/\r?\n/)) {
78
- const t = line.trim()
79
- if (!t) continue
80
- try { const n = normalizeRecord(JSON.parse(t), cfg); if (n) out.push(n) } catch { /* skip malformed */ }
81
- }
82
- return out
83
- }
84
-
85
- // ─── deploy (run numbat commands on a host) ─────────────────────────────────
86
-
87
- /** Build the numbat CLI argv for a deploy action. Pure — testable. */
88
- export function buildDeployCommand(action, opts = {}) {
89
- const agent = opts.agent || 'codex'
90
- switch (action) {
91
- case 'inventory': return ['agents']
92
- case 'scan': return opts.agent ? ['scan', '--agent', agent] : ['scan']
93
- case 'install-monitor': return ['hook', 'install', '--agent', agent, '--emit', opts.emit ?? 'all']
94
- case 'install-enforce': return ['hook', 'install', '--agent', agent, '--emit', opts.emit ?? 'all', ...(opts.rulesDir ? ['--rules-dir', opts.rulesDir] : []), '--enforce']
95
- case 'status': return ['hook', 'status', '--agent', agent]
96
- case 'uninstall': return ['hook', 'uninstall', '--agent', agent]
97
- default: throw new Error(`unknown numbat deploy action: ${action}`)
98
- }
99
- }
100
-
101
- /** Run a numbat command via the plugin's exec capability (local or remote host). */
102
- async function runNumbat(ctx, cfg, argv, target) {
103
- const cmdline = [cfg.binaryPath, ...argv].map((a) => (/\s/.test(a) ? `"${a}"` : a)).join(' ')
104
- // Prefer the plugin exec/runCommand capability when available (policy-gated).
105
- if (typeof ctx.runCommand === 'function') {
106
- return ctx.runCommand({ command: cmdline, target })
107
- }
108
- if (typeof ctx.exec === 'function') {
109
- return ctx.exec(cmdline, { target })
110
- }
111
- // Local fallback via child_process.
112
- const { execFile } = require('node:child_process')
113
- return new Promise((resolve) => {
114
- execFile(cfg.binaryPath, argv, { timeout: 60000 }, (err, stdout, stderr) => {
115
- resolve({ ok: !err, exitCode: err?.code ?? 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? ''), error: err ? String(err.message) : undefined })
116
- })
117
- })
118
- }
119
-
120
- // ─── plugin registration ────────────────────────────────────────────────────
121
-
122
- async function guarded(fn, log) {
123
- try { return await fn() } catch (e) {
124
- const msg = e?.message ?? String(e)
125
- log?.(`[numbat] ${msg}`)
126
- return { error: msg, hint: 'Is numbat installed? (go install github.com/perplexityai/numbat/cmd/numbat@latest, or download a release). Configure settings.numbat.' }
127
- }
128
- }
129
-
130
- export function register(ctx) {
131
- const { registerTool, registerTrigger, registerPanel, log } = ctx
132
- const cfg = resolveConfig(ctx)
133
-
134
- registerTool({
135
- name: 'numbat_health',
136
- description: 'Check the numbat binary is present and report its version + the configured records path.',
137
- params: {},
138
- handler: async () => guarded(async () => {
139
- const r = await runNumbat(ctx, cfg, ['version'])
140
- return { binaryPath: cfg.binaryPath, recordsPath: cfg.recordsPath, ...r }
141
- }, log),
142
- })
143
-
144
- registerTool({
145
- name: 'numbat_deploy',
146
- description: 'Deploy/manage numbat on a host: inventory agents, scan, install monitor-only or enforce hooks, check status, or uninstall. Runs the numbat CLI via the policy-gated exec path (local or a target host).',
147
- params: {
148
- action: { type: 'string', description: 'inventory | scan | install-monitor | install-enforce | status | uninstall' },
149
- agent: { type: 'string', description: 'Target agent (e.g. codex)', optional: true },
150
- target: { type: 'string', description: 'Host/terminal to run on (default local)', optional: true },
151
- rulesDir: { type: 'string', description: 'Custom rules dir (for enforce)', optional: true },
152
- emit: { type: 'string', description: 'Emit mode (default all)', optional: true },
153
- },
154
- handler: async (p) => guarded(async () => {
155
- if (!p?.action) return { error: 'numbat_deploy needs an action' }
156
- const argv = buildDeployCommand(p.action, p)
157
- const r = await runNumbat(ctx, cfg, argv, p?.target)
158
- return { action: p.action, argv, ...r }
159
- }, log),
160
- })
161
-
162
- registerTool({
163
- name: 'numbat_ingest',
164
- description: 'Ingest Numbat NDJSON records (events/findings/enforcement/indicators) — normalize them into RTerm findings and fire the numbat_finding trigger for each. Pass ndjson text directly or read from the configured records file.',
165
- params: {
166
- ndjson: { type: 'string', description: 'NDJSON records (one JSON per line)', optional: true },
167
- fromFile: { type: 'boolean', description: 'Read from recordsPath instead of inline ndjson', optional: true },
168
- },
169
- handler: async (p) => guarded(async () => {
170
- let text = p?.ndjson
171
- if (!text && (p?.fromFile || !p?.ndjson)) {
172
- const fs = require('node:fs')
173
- try { text = fs.readFileSync(cfg.recordsPath, 'utf8') } catch { text = '' }
174
- }
175
- const findings = parseNdjson(text ?? '', cfg)
176
- // Fire a trigger event per finding (the trigger engine routes these).
177
- if (typeof ctx.emitEvent === 'function') {
178
- for (const f of findings) ctx.emitEvent({ source: 'numbat', ...f })
179
- }
180
- return { ingested: findings.length, findings }
181
- }, log),
182
- })
183
-
184
- registerTool({
185
- name: 'numbat_findings_summary',
186
- description: 'Summarize Numbat findings from the records file by severity + rule + agent (quick threat picture).',
187
- params: {},
188
- handler: async () => guarded(async () => {
189
- const fs = require('node:fs')
190
- let text = ''
191
- try { text = fs.readFileSync(cfg.recordsPath, 'utf8') } catch { /* none */ }
192
- const findings = parseNdjson(text, { ...cfg, minSeverity: 'info' })
193
- const bySeverity = {}
194
- const byRule = {}
195
- const byAgent = {}
196
- for (const f of findings) {
197
- bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1
198
- if (f.ruleId) byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1
199
- if (f.agent) byAgent[f.agent] = (byAgent[f.agent] ?? 0) + 1
200
- }
201
- return { total: findings.length, bySeverity, byRule, byAgent, recordsPath: cfg.recordsPath }
202
- }, log),
203
- })
204
-
205
- registerTrigger({
206
- name: 'numbat_finding',
207
- description: 'Fires when a Numbat detection (finding/enforcement/indicator, or high-severity event) is ingested. Use to auto-remediate: isolate the host, kill the agent, open an incident, or run a playbook.',
208
- match: (event) => {
209
- if (event?.source !== 'numbat') return false
210
- return severityAtLeast(event.severity, 'medium')
211
- },
212
- action: 'propose-change',
213
- })
214
-
215
- registerPanel({
216
- name: 'numbat-findings',
217
- title: 'Numbat Findings',
218
- render: (data) => {
219
- const rows = (Array.isArray(data) ? data : []).map((f) =>
220
- `<tr><td>${f.severity ?? ''}</td><td>${f.title ?? ''}</td><td>${f.agent ?? ''}</td><td>${f.host ?? ''}</td></tr>`
221
- ).join('')
222
- return `<div class="numbat-findings"><h3>Numbat Findings</h3><p>Records: ${cfg.recordsPath}</p><table><thead><tr><th>Severity</th><th>Title</th><th>Agent</th><th>Host</th></tr></thead><tbody>${rows}</tbody></table></div>`
223
- },
224
- })
225
-
226
- log(`[numbat] numbat-bridge registered: 4 tools, 1 trigger, 1 panel (bin=${cfg.binaryPath})`)
227
- }
228
-
229
- export default { register, resolveConfig, normalizeRecord, parseNdjson, buildDeployCommand }