rterm-backend 3.1.6 → 3.1.7
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-backend",
|
|
3
|
-
"version": "3.1.
|
|
4
|
-
"description": "Headless AI-native backend for RTerm / neuralOS — v3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
|
+
"description": "Headless AI-native backend for RTerm / neuralOS — v3.1.7: always-on full-duplex Synapse agent (auto-start responder on boot when synapse.enabled + autoServe). RTerm tasks other agents AND is tasked by them from boot. 11 plugins. Transports (SSH/serial/local), SQLite, and NATS libs install automatically.",
|
|
5
5
|
"main": "bin/gybackend.cjs",
|
|
6
6
|
"bin": { "gybackend": "bin/gybackend.cjs" },
|
|
7
7
|
"license": "MIT",
|
|
@@ -46,6 +46,8 @@ export function resolveConfig(ctx = {}, env = process.env) {
|
|
|
46
46
|
agentId: block.agentId || env.SYNAPSE_AGENT_ID || 'rterm-001',
|
|
47
47
|
auth: block.auth || undefined,
|
|
48
48
|
enabled: block.enabled !== false,
|
|
49
|
+
/** auto-start the full-duplex responder on boot (default true when enabled). */
|
|
50
|
+
autoServe: block.autoServe !== false,
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -264,20 +266,46 @@ export function register(ctx) {
|
|
|
264
266
|
// ─── full-duplex agent capabilities (responder, emit/subscribe, reputation, governance) ───
|
|
265
267
|
const repStore = new ReputationStore()
|
|
266
268
|
let responderStop = null
|
|
269
|
+
let servingSkills = []
|
|
270
|
+
|
|
271
|
+
/** Start (or restart) the responder with the given skills. Idempotent. */
|
|
272
|
+
async function serveSkills(skills) {
|
|
273
|
+
const nc = await connectMesh(ctx)
|
|
274
|
+
const serveCtx = { ...ctx, rtermSkills: skills ?? ctx.rtermSkills ?? {} }
|
|
275
|
+
if (responderStop) responderStop()
|
|
276
|
+
responderStop = await startResponder(nc, cfg, serveCtx, log)
|
|
277
|
+
servingSkills = Object.keys(serveCtx.rtermSkills)
|
|
278
|
+
return servingSkills
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Default skills RTerm serves when auto-starting (status + discover + dispatch). */
|
|
282
|
+
function defaultServeSkills() {
|
|
283
|
+
return {
|
|
284
|
+
status: async () => ({ up: true, agent: cfg.agentId, ts: new Date().toISOString() }),
|
|
285
|
+
discover: async (inp) => ({ agents: await discoverAgents(ctx, inp ?? {}) }),
|
|
286
|
+
...(ctx.rtermSkills ?? {}),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
267
289
|
|
|
268
290
|
registerTool({
|
|
269
291
|
name: 'synapse_serve',
|
|
270
|
-
description: 'Start RTerm as a full Synapse agent: listen on mesh.agent.{id}.inbox and respond() to incoming Synapse requests by mapping them to RTerm skills (playbooks/tools via ctx.rtermSkills / getRtermSkills). Bidirectional federation — other agents can now task RTerm.',
|
|
292
|
+
description: 'Start RTerm as a full Synapse agent: listen on mesh.agent.{id}.inbox and respond() to incoming Synapse requests by mapping them to RTerm skills (playbooks/tools via ctx.rtermSkills / getRtermSkills). Bidirectional federation — other agents can now task RTerm. Idempotent; auto-starts on boot when synapse.enabled and autoServe are true.',
|
|
271
293
|
params: {
|
|
272
|
-
skills: { type: 'object', description: 'Map of skillId -> async (input, ctx) => output, the skills RTerm serves', optional: true },
|
|
294
|
+
skills: { type: 'object', description: 'Map of skillId -> async (input, ctx) => output, the skills RTerm serves (defaults to status+discover)', optional: true },
|
|
273
295
|
},
|
|
274
296
|
handler: async (p) => guarded(async () => {
|
|
297
|
+
const skills = await serveSkills(p?.skills ?? defaultServeSkills())
|
|
298
|
+
return { serving: true, inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills, note: 'RTerm is now a full Synapse agent (responder live)' }
|
|
299
|
+
}, log),
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
registerTool({
|
|
303
|
+
name: 'synapse_serve_status',
|
|
304
|
+
description: 'Report whether the Synapse responder is live (serving on mesh.agent.{id}.inbox) and which skills it serves.',
|
|
305
|
+
params: {},
|
|
306
|
+
handler: async () => guarded(async () => {
|
|
275
307
|
const nc = await connectMesh(ctx)
|
|
276
|
-
|
|
277
|
-
if (responderStop) responderStop() // idempotent: restart with fresh skills
|
|
278
|
-
responderStop = await startResponder(nc, cfg, serveCtx, log)
|
|
279
|
-
const skillIds = Object.keys(serveCtx.rtermSkills)
|
|
280
|
-
return { serving: true, inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills: skillIds, note: 'RTerm is now a full Synapse agent (responder live)' }
|
|
308
|
+
return { serving: responderStop !== null, connected: !nc.isClosed(), inbox: `${cfg.prefix}.agent.${cfg.agentId}.inbox`, skills: servingSkills, autoServe: cfg.autoServe }
|
|
281
309
|
}, log),
|
|
282
310
|
})
|
|
283
311
|
|
|
@@ -382,7 +410,16 @@ export function register(ctx) {
|
|
|
382
410
|
},
|
|
383
411
|
})
|
|
384
412
|
|
|
385
|
-
log(`[synapse] synapse-bridge registered:
|
|
413
|
+
log(`[synapse] synapse-bridge registered: 12 tools, 1 trigger, 1 panel (agent=${cfg.agentId}, prefix=${cfg.prefix}, full-duplex)`)
|
|
414
|
+
|
|
415
|
+
// ─── auto-start the full-duplex responder on boot (when enabled + autoServe) ───
|
|
416
|
+
// Makes "be tasked by them" always-on, not opt-in per session. Best-effort: a
|
|
417
|
+
// failed auto-start logs but never blocks plugin registration (server may be down).
|
|
418
|
+
if (cfg.enabled && cfg.autoServe) {
|
|
419
|
+
serveSkills(defaultServeSkills())
|
|
420
|
+
.then((skills) => log(`[synapse] auto-started responder on ${cfg.prefix}.agent.${cfg.agentId}.inbox (skills: ${skills.join(', ')})`))
|
|
421
|
+
.catch((e) => log(`[synapse] auto-serve deferred: ${e?.message ?? e} (responder will start on first synapse_serve call)`))
|
|
422
|
+
}
|
|
386
423
|
}
|
|
387
424
|
|
|
388
425
|
export default { register, resolveConfig, envelope, discoverAgents, dispatchTask, registerSelf }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synapse-bridge",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Synapse mesh bridge for RTerm — full-duplex Synapse agent: discover live agents, dispatch tasks, register RTerm as a mesh agent, AND serve as one (respond to inbound requests), emit/subscribe mesh events, EXT-REPUTATION scoring, and EXT-GOVERNANCE approvals. Speaks the Synapse protocol (v0.3.0) over a shared NATS server using the v3.1.2 auth/request-reply/JetStream transport. Config in Settings (synapse block); secrets via the vault.",
|
|
4
|
+
"description": "Synapse mesh bridge for RTerm — full-duplex Synapse agent (auto-started on boot): discover live agents, dispatch tasks, register RTerm as a mesh agent, AND serve as one (respond to inbound requests, auto-started), emit/subscribe mesh events, EXT-REPUTATION scoring, and EXT-GOVERNANCE approvals. Speaks the Synapse protocol (v0.3.0) over a shared NATS server using the v3.1.2 auth/request-reply/JetStream transport. Config in Settings (synapse block); secrets via the vault.",
|
|
5
5
|
"entry": "index.mjs",
|
|
6
6
|
"tools": [
|
|
7
7
|
"synapse_health",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"synapse_register",
|
|
11
11
|
"synapse_agents_summary",
|
|
12
12
|
"synapse_serve",
|
|
13
|
+
"synapse_serve_status",
|
|
13
14
|
"synapse_emit",
|
|
14
15
|
"synapse_subscribe",
|
|
15
16
|
"synapse_reputation",
|
|
@@ -9,9 +9,28 @@ function eq(a, b, m) { if (a !== b) throw new Error(`${m ?? 'eq'}: expected ${JS
|
|
|
9
9
|
function fakeConn() {
|
|
10
10
|
const published = []
|
|
11
11
|
const requests = new Map()
|
|
12
|
+
const subs = new Map()
|
|
12
13
|
return {
|
|
13
14
|
isClosed: () => false,
|
|
14
15
|
publish(subject, data) { published.push({ subject, data: JSON.parse(new TextDecoder().decode(data)) }) },
|
|
16
|
+
subscribe(subject) {
|
|
17
|
+
const sub = {
|
|
18
|
+
async *[Symbol.asyncIterator]() {
|
|
19
|
+
while (true) {
|
|
20
|
+
const m = await new Promise((res) => {
|
|
21
|
+
const l = subs.get(subject) ?? []; subs.set(subject, l); l.push(res)
|
|
22
|
+
})
|
|
23
|
+
yield m
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
unsubscribe: () => subs.delete(subject),
|
|
27
|
+
}
|
|
28
|
+
return sub
|
|
29
|
+
},
|
|
30
|
+
_deliver(subject, env) {
|
|
31
|
+
const data = new TextEncoder().encode(JSON.stringify(env))
|
|
32
|
+
for (const fn of subs.get(subject) ?? []) fn({ data, subject, respond: (p) => { published.push({ subject: '_reply', env: JSON.parse(new TextDecoder().decode(p)) }) } })
|
|
33
|
+
},
|
|
15
34
|
async request(subject, data, _opts) {
|
|
16
35
|
const h = requests.get(subject)
|
|
17
36
|
const reply = h ? h(JSON.parse(new TextDecoder().decode(data))) : { ok: true }
|
|
@@ -69,12 +88,12 @@ test('envelope has Synapse v0.3.0 shape', () => {
|
|
|
69
88
|
|
|
70
89
|
// ─── register wiring ────────────────────────────────────────────────────────
|
|
71
90
|
|
|
72
|
-
test('register wires
|
|
91
|
+
test('register wires 12 tools, 1 trigger, 1 panel (full-duplex)', () => {
|
|
73
92
|
const conn = fakeConn()
|
|
74
93
|
const { tools, triggers, panels, ctx } = mkCtx({}, conn)
|
|
75
94
|
register(ctx)
|
|
76
|
-
eq(tools.size,
|
|
77
|
-
for (const n of ['synapse_health', 'synapse_discover', 'synapse_dispatch', 'synapse_register', 'synapse_agents_summary', 'synapse_serve', 'synapse_emit', 'synapse_subscribe', 'synapse_reputation', 'synapse_request_approval', 'synapse_approve']) assert(tools.has(n), `missing ${n}`)
|
|
95
|
+
eq(tools.size, 12, 'tool count')
|
|
96
|
+
for (const n of ['synapse_health', 'synapse_discover', 'synapse_dispatch', 'synapse_register', 'synapse_agents_summary', 'synapse_serve', 'synapse_serve_status', 'synapse_emit', 'synapse_subscribe', 'synapse_reputation', 'synapse_request_approval', 'synapse_approve']) assert(tools.has(n), `missing ${n}`)
|
|
78
97
|
eq(triggers.length, 1, 'trigger count')
|
|
79
98
|
eq(triggers[0].name, 'synapse_mesh_event', 'trigger name')
|
|
80
99
|
eq(panels.length, 1, 'panel count')
|
|
@@ -83,6 +102,7 @@ test('register wires 11 tools, 1 trigger, 1 panel (full-duplex)', () => {
|
|
|
83
102
|
// ─── discover ───────────────────────────────────────────────────────────────
|
|
84
103
|
|
|
85
104
|
test('synapse_discover returns agents from registry', async () => {
|
|
105
|
+
__setConnForTest(null)
|
|
86
106
|
const conn = fakeConn()
|
|
87
107
|
conn._on('mesh.registry.discover', () => [
|
|
88
108
|
{ id: 'grip-cli-001', name: 'Grip CLI', skills: [{ id: 'himalaya' }] },
|
|
@@ -206,6 +226,50 @@ test('a failed connect is not cached — the next call retries', async () => {
|
|
|
206
226
|
if (attempts !== 2) throw new Error(`expected 2 attempts (fail + retry), got ${attempts}`)
|
|
207
227
|
})
|
|
208
228
|
|
|
229
|
+
// ─── auto-start responder on boot (serveSkills + autoServe) ─────────────────
|
|
230
|
+
|
|
231
|
+
test('autoServe: register() auto-starts the responder when enabled+autoServe (default skills)', async () => {
|
|
232
|
+
__setConnForTest(null)
|
|
233
|
+
const conn = fakeConn()
|
|
234
|
+
conn._on('mesh.registry.discover', () => [])
|
|
235
|
+
const { tools, logs, ctx } = mkCtx({}, conn)
|
|
236
|
+
register(ctx)
|
|
237
|
+
// auto-start is async (serveSkills → connectMesh → startResponder) — wait for it to settle
|
|
238
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
239
|
+
const r = await tools.get('synapse_serve_status').handler({})
|
|
240
|
+
eq(r.serving, true, 'responder auto-started (serving=true)')
|
|
241
|
+
assert(r.skills.includes('status'), 'default skills include status')
|
|
242
|
+
assert(r.skills.includes('discover'), 'default skills include discover')
|
|
243
|
+
eq(r.autoServe, true, 'autoServe true by default')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('autoServe: disabled when synapse.autoServe=false (no auto-start)', async () => {
|
|
247
|
+
__setConnForTest(null)
|
|
248
|
+
const conn = fakeConn()
|
|
249
|
+
const { tools, ctx } = mkCtx({ autoServe: false }, conn)
|
|
250
|
+
register(ctx)
|
|
251
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
252
|
+
const r = await tools.get('synapse_serve_status').handler({})
|
|
253
|
+
eq(r.serving, false, 'responder NOT auto-started when autoServe=false')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('autoServe: a failed auto-start does not break register (best-effort)', async () => {
|
|
257
|
+
__setConnForTest(null)
|
|
258
|
+
let failConnect = true
|
|
259
|
+
const conn = fakeConn()
|
|
260
|
+
const ctx = {
|
|
261
|
+
settings: { synapse: { url: 'nats://down:4222' } },
|
|
262
|
+
natsConnect: async () => { if (failConnect) throw new Error('server down'); return conn },
|
|
263
|
+
registerTool: () => {}, registerTrigger: () => {}, registerPanel: () => {}, log: () => {},
|
|
264
|
+
}
|
|
265
|
+
// register() must not throw even though the auto-start connection fails
|
|
266
|
+
const { register } = await import('./index.mjs')
|
|
267
|
+
let threw = false
|
|
268
|
+
try { register(ctx) } catch { threw = true }
|
|
269
|
+
assert(!threw, 'register() must not throw on auto-serve connection failure')
|
|
270
|
+
await new Promise((r) => setTimeout(r, 20)) // let the auto-start promise reject (handled)
|
|
271
|
+
})
|
|
272
|
+
|
|
209
273
|
// ─── runner ─────────────────────────────────────────────────────────────────
|
|
210
274
|
async function main() {
|
|
211
275
|
let pass = 0, fail = 0
|