rterm-backend 3.8.6 → 3.8.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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-backend",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.7",
|
|
4
4
|
"description": "AI-native terminal & agentic-AI operations platform for Forward Deployed Engineers & SREs: AIOps closed-loop remediation, AI SRE, self-healing infrastructure, runbook automation, ChatOps; executes over SSH/WinRM/serial under policy with tamper-evident audit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"forward-deployed-engineer",
|
|
@@ -185,13 +185,30 @@ export async function discoverPeers(ctx, filter = {}) {
|
|
|
185
185
|
if (isPublishAck(msg.data)) continue
|
|
186
186
|
const env = uj(msg.data)
|
|
187
187
|
if (env?.type !== 'respond' || env?.from === cfg.agentId) continue
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
// SHAPE: the gateway's handleDiscoverRequest replies with the BARE
|
|
189
|
+
// manifest as the payload (attachPayload(reply, manifest)), and a
|
|
190
|
+
// registry may reply with {agents:[...]}. manifestsFrom on the gateway
|
|
191
|
+
// side accepts both; so do we. Some peers may still nest under
|
|
192
|
+
// payload.manifest, so accept that legacy shape too.
|
|
193
|
+
const p = env?.payload
|
|
194
|
+
const candidates = Array.isArray(p?.agents) ? p.agents
|
|
195
|
+
: (p?.id || p?.identity) ? [p]
|
|
196
|
+
: (p?.manifest?.id || p?.manifest?.identity) ? [p.manifest]
|
|
197
|
+
: []
|
|
198
|
+
for (const m of candidates) {
|
|
199
|
+
const id = m?.id ?? m?.identity
|
|
200
|
+
if (id && id !== cfg.agentId) replies.push(m)
|
|
201
|
+
}
|
|
190
202
|
} catch { /* ignore malformed */ }
|
|
191
203
|
}
|
|
192
204
|
})()
|
|
193
205
|
loop.catch(() => {})
|
|
194
|
-
|
|
206
|
+
// BUG FIX (found live against the real gateway): the discover publish MUST
|
|
207
|
+
// carry a reply subject. The gateway's handleDiscoverRequest returns
|
|
208
|
+
// immediately when message.Reply == "" ("a discovery query with no reply
|
|
209
|
+
// subject cannot be answered"), so the old bare publish made discovery
|
|
210
|
+
// silently return zero peers even though every peer was listening.
|
|
211
|
+
nc.publish(`${cfg.prefix}.registry.discover`, j(signed), { reply: '_INBOX.discover' })
|
|
195
212
|
await new Promise((r) => setTimeout(r, windowMs))
|
|
196
213
|
try { replySub.unsubscribe() } catch { /* best-effort */ }
|
|
197
214
|
|
|
@@ -26,6 +26,7 @@ import { join } from 'node:path'
|
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
28
|
AUTH_KEYS,
|
|
29
|
+
buildManifest,
|
|
29
30
|
buildRespond,
|
|
30
31
|
checkIdentity,
|
|
31
32
|
envelope,
|
|
@@ -211,6 +212,61 @@ function ok(cond: unknown, label: string, note = ''): void {
|
|
|
211
212
|
ok(threw, 'a tampered identity file is rejected on load')
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
// ─── A2. Manifest shape contract (the gateway's Manifest struct) ─────────────
|
|
216
|
+
// Found live: the plugin sent `identity` where the gateway's Manifest
|
|
217
|
+
// unmarshals `json:"id"`, and bare-string skills where the gateway expects
|
|
218
|
+
// []{id,name,description} objects. manifestMatches drops a manifest whose ID
|
|
219
|
+
// is empty, so the peer was INVISIBLE to gateway discovery. These pin the
|
|
220
|
+
// corrected contract so it cannot regress.
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
const cfg = { agentId: 'rterm/xcheck/manifest', name: 'XCheck', fingerprint: 'sha256:abc123' }
|
|
224
|
+
const m = buildManifest(cfg, ['ping', 'describe', 'status', 'invoke'])
|
|
225
|
+
ok(m.id === 'rterm/xcheck/manifest',
|
|
226
|
+
'manifest uses id (json:"id") — the gateway drops a manifest with empty ID',
|
|
227
|
+
JSON.stringify(Object.keys(m)))
|
|
228
|
+
ok(!('identity' in m), 'manifest must NOT carry a legacy identity key')
|
|
229
|
+
ok(m.name === 'XCheck', 'manifest name is the display name')
|
|
230
|
+
ok(Array.isArray(m.capabilities) && m.capabilities.includes('rterm'),
|
|
231
|
+
'manifest advertises the rterm capability')
|
|
232
|
+
ok(Array.isArray(m.skills) && m.skills.length === 4
|
|
233
|
+
&& m.skills.every((s: { id: string }) => typeof s?.id === 'string'),
|
|
234
|
+
'skills are []{id,...} objects, not bare strings (the gateway Skill struct unmarshals objects)',
|
|
235
|
+
JSON.stringify(m.skills))
|
|
236
|
+
ok(m.fingerprint === 'sha256:abc123', 'manifest carries the fingerprint for TOFU pinning')
|
|
237
|
+
ok(Array.isArray(m.local_agents) && m.local_agents[0]?.id === cfg.agentId,
|
|
238
|
+
'local_agents entries use id too')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A string skill list must be normalized to objects without dropping any.
|
|
242
|
+
{
|
|
243
|
+
const m = buildManifest({ agentId: 'a' }, ['ping', 'status'])
|
|
244
|
+
ok(m.skills.length === 2 && m.skills[0].id === 'ping' && m.skills[1].id === 'status',
|
|
245
|
+
'string skill ids normalize to {id,name} objects in order')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A discover reply in the GATEWAY's bare-manifest shape must be accepted by
|
|
249
|
+
// the plugin's parser (the old parser only read payload.manifest). The
|
|
250
|
+
// candidates logic lives inline in discoverPeers; this pins the accepted
|
|
251
|
+
// payload shapes by constructing them the way the gateway does.
|
|
252
|
+
{
|
|
253
|
+
const bare = { id: 'gateway/edge-1', name: 'Edge', skills: [{ id: 'ping' }] }
|
|
254
|
+
const agents = { agents: [{ id: 'gateway/edge-2', name: 'Edge2' }] }
|
|
255
|
+
const legacy = { manifest: { id: 'old/peer', name: 'Old' } }
|
|
256
|
+
const shapes = [bare, agents, legacy]
|
|
257
|
+
for (const p of shapes) {
|
|
258
|
+
const candidates = Array.isArray((p as { agents?: unknown[] }).agents)
|
|
259
|
+
? (p as { agents: unknown[] }).agents
|
|
260
|
+
: ((p as { id?: string }).id || (p as { identity?: string }).identity)
|
|
261
|
+
? [p]
|
|
262
|
+
: ((p as { manifest?: { id?: string } }).manifest?.id
|
|
263
|
+
|| (p as { manifest?: { identity?: string } }).manifest?.identity)
|
|
264
|
+
? [(p as { manifest: unknown }).manifest]
|
|
265
|
+
: []
|
|
266
|
+
ok(candidates.length > 0, `discover parser accepts payload shape ${JSON.stringify(Object.keys(p))}`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
214
270
|
// ─── B. Go cross-check against the REAL gateway ──────────────────────────────
|
|
215
271
|
|
|
216
272
|
const RP_CHECKOUT = process.env.RP_CHECKOUT ?? join(process.env.HOME ?? '', 'work', 'ReactorPro')
|
|
@@ -299,31 +299,50 @@ export function startHeartbeat(nc, cfg, identity, intervalMs = 30000, log = () =
|
|
|
299
299
|
|
|
300
300
|
// ─── 5. REGISTRY ─────────────────────────────────────────────────────────────
|
|
301
301
|
|
|
302
|
-
/** The manifest this RTerm advertises.
|
|
302
|
+
/** The manifest this RTerm advertises.
|
|
303
|
+
*
|
|
304
|
+
* SHAPE CONTRACT (mirrors the gateway's Manifest struct, envelope.go:146):
|
|
305
|
+
* id — json:"id" (NOT "identity"; manifestMatches drops a
|
|
306
|
+
* manifest whose ID is empty, so the old `identity` key made
|
|
307
|
+
* this peer invisible to gateway discovery)
|
|
308
|
+
* skills — []{id, name?, description?} objects, NOT bare strings
|
|
309
|
+
* (the gateway's Skill struct unmarshals objects)
|
|
310
|
+
* fingerprint — json:"fingerprint,omitempty" — pins trust-on-first-use
|
|
311
|
+
* The register/discover payloads are the BARE manifest (the gateway's
|
|
312
|
+
* Register/handleDiscoverRequest both attachPayload(manifest) directly), so
|
|
313
|
+
* callers must not nest it under a "manifest" key. */
|
|
303
314
|
export function buildManifest(cfg, skillIds) {
|
|
315
|
+
const skills = (skillIds ?? []).map((s) =>
|
|
316
|
+
typeof s === 'string' ? { id: s, name: s } : s)
|
|
304
317
|
return {
|
|
305
|
-
|
|
318
|
+
id: cfg.agentId,
|
|
306
319
|
name: cfg.name ?? cfg.agentId,
|
|
307
320
|
capabilities: ['rterm', 'agent', ...(cfg.capabilities ?? [])],
|
|
308
|
-
skills
|
|
321
|
+
skills,
|
|
309
322
|
fingerprint: cfg.fingerprint ?? null,
|
|
310
323
|
local_agents: [{ id: cfg.agentId, name: cfg.name ?? cfg.agentId, capabilities: ['rterm', 'agent'] }],
|
|
311
324
|
}
|
|
312
325
|
}
|
|
313
326
|
|
|
314
|
-
/** Register with the mesh registry (mesh.registry.register).
|
|
327
|
+
/** Register with the mesh registry (mesh.registry.register).
|
|
328
|
+
* Payload is the BARE manifest — the gateway's Register does
|
|
329
|
+
* attachPayload(envelope, a.Manifest()) with no wrapper object. */
|
|
315
330
|
export function registerManifest(nc, cfg, identity, manifest) {
|
|
316
|
-
const env = envelope('register',
|
|
331
|
+
const env = envelope('register', manifest, cfg, { to: 'REGISTRY' })
|
|
317
332
|
nc.publish(`${cfg.prefix}.registry.register`, j(signEnvelope(env, identity)))
|
|
318
333
|
}
|
|
319
334
|
|
|
320
335
|
/** Graceful exit. */
|
|
321
336
|
export function deregisterManifest(nc, cfg, identity) {
|
|
322
|
-
const env = envelope('register', {
|
|
337
|
+
const env = envelope('register', { id: cfg.agentId, gone: true }, cfg, { to: 'REGISTRY' })
|
|
323
338
|
nc.publish(`${cfg.prefix}.registry.deregister`, j(signEnvelope(env, identity)))
|
|
324
339
|
}
|
|
325
340
|
|
|
326
|
-
/** Answer discovery broadcasts with our manifest.
|
|
341
|
+
/** Answer discovery broadcasts with our manifest.
|
|
342
|
+
* The reply payload is the BARE manifest — the gateway's
|
|
343
|
+
* handleDiscoverRequest does attachPayload(reply, manifest), and its
|
|
344
|
+
* manifestsFrom accepts either {agents:[...]} or a bare manifest with
|
|
345
|
+
* id != "". Nesting under "manifest" would make it invisible. */
|
|
327
346
|
export async function answerDiscovery(nc, cfg, identity, manifest, log = () => {}) {
|
|
328
347
|
const subject = `${cfg.prefix}.registry.discover`
|
|
329
348
|
const sub = nc.subscribe(subject)
|
|
@@ -336,7 +355,7 @@ export async function answerDiscovery(nc, cfg, identity, manifest, log = () => {
|
|
|
336
355
|
const req = uj(msg.data)
|
|
337
356
|
if (req?.from === cfg.agentId) continue // don't answer ourselves
|
|
338
357
|
if (msg.reply) {
|
|
339
|
-
const env = envelope('respond',
|
|
358
|
+
const env = envelope('respond', manifest, cfg, { to: req?.from, in_reply_to: req?.id })
|
|
340
359
|
nc.publish(msg.reply, j(signEnvelope(env, identity)))
|
|
341
360
|
}
|
|
342
361
|
} catch (e) { log(`[reactorpro] discovery answer failed: ${e?.message ?? e}`) }
|