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.
- package/LICENSE +202 -0
- package/README.md +48 -347
- package/cli.js +78 -0
- package/engine.js +124 -0
- package/index.js +160 -0
- package/package.json +37 -43
- package/test.js +20 -0
- package/tools.js +47 -0
- package/bin/gybackend.cjs +0 -398530
- package/plugins/agentspan-bridge/agentspan-bridge.extreme.spec.ts +0 -328
- package/plugins/agentspan-bridge/agentspan-bridge.phase2.extreme.spec.ts +0 -282
- package/plugins/agentspan-bridge/conductorClient.mjs +0 -182
- package/plugins/agentspan-bridge/index.mjs +0 -389
- package/plugins/agentspan-bridge/playbookToWorkflowDef.mjs +0 -209
- package/plugins/agentspan-bridge/plugin.json +0 -27
- package/plugins/fraudops/index.d.mts +0 -9
- package/plugins/fraudops/index.mjs +0 -185
- package/plugins/fraudops/plugin.json +0 -10
- package/plugins/iam-connector/index.d.mts +0 -10
- package/plugins/iam-connector/index.mjs +0 -229
- package/plugins/iam-connector/plugin.json +0 -10
- package/plugins/mitmproxy-bridge/index.d.mts +0 -20
- package/plugins/mitmproxy-bridge/index.mjs +0 -256
- package/plugins/mitmproxy-bridge/plugin.json +0 -17
- package/plugins/monid-bridge/index.mjs +0 -228
- package/plugins/monid-bridge/monid-bridge.extreme.spec.mjs +0 -75
- package/plugins/monid-bridge/plugin.json +0 -15
- package/plugins/netdata-rterm/index.mjs +0 -229
- package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +0 -252
- package/plugins/netdata-rterm/plugin.json +0 -22
- package/plugins/netexec-bridge/index.d.mts +0 -28
- package/plugins/netexec-bridge/index.mjs +0 -325
- package/plugins/netexec-bridge/plugin.json +0 -20
- package/plugins/neuralos/index.mjs +0 -137
- package/plugins/neuralos/neuralos.extreme.spec.mts +0 -386
- package/plugins/neuralos/neuralosEngine.mjs +0 -369
- package/plugins/neuralos/plugin.json +0 -15
- package/plugins/numbat-bridge/index.mjs +0 -229
- package/plugins/numbat-bridge/numbat-bridge.extreme.spec.mjs +0 -148
- package/plugins/numbat-bridge/plugin.json +0 -21
- package/plugins/patch-manager/index.d.mts +0 -10
- package/plugins/patch-manager/index.mjs +0 -288
- package/plugins/patch-manager/plugin.json +0 -10
- package/plugins/promptfoo-redteam/index.d.mts +0 -15
- package/plugins/promptfoo-redteam/index.mjs +0 -277
- package/plugins/promptfoo-redteam/plugin.json +0 -16
- package/plugins/reactorpro-bridge/index.mjs +0 -499
- package/plugins/reactorpro-bridge/plugin.json +0 -26
- package/plugins/reactorpro-bridge/reactorpro-bridge.extreme.spec.mts +0 -409
- package/plugins/reactorpro-bridge/reactorproAgent.mjs +0 -399
- package/plugins/request-router/index.d.mts +0 -9
- package/plugins/request-router/index.mjs +0 -239
- package/plugins/request-router/plugin.json +0 -10
- package/plugins/sample-k8s-slo/index.ts +0 -58
- package/plugins/sample-k8s-slo/plugin.json +0 -11
- package/plugins/sop-assistant/index.d.mts +0 -9
- package/plugins/sop-assistant/index.mjs +0 -325
- package/plugins/sop-assistant/plugin.json +0 -10
- package/plugins/synapse-bridge/index.mjs +0 -484
- package/plugins/synapse-bridge/plugin.json +0 -29
- package/plugins/synapse-bridge/synapse-bridge.extreme.spec.mjs +0 -204
- package/plugins/synapse-bridge/synapseAgent.extreme.spec.mjs +0 -236
- package/plugins/synapse-bridge/synapseAgent.mjs +0 -254
- package/plugins/web-intel/index.mjs +0 -381
- package/plugins/web-intel/plugin.json +0 -28
- package/plugins/web-intel/sidecar.mjs +0 -134
- package/plugins/web-intel/web-intel.extreme.spec.ts +0 -263
- package/plugins/web-intel/wigoloClient.mjs +0 -145
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* reactorpro-bridge — wire compatibility with the ReactorPro gateway.
|
|
3
|
-
*
|
|
4
|
-
* This is the contract test for v3.8.5: the plugin must be interoperable
|
|
5
|
-
* with the REAL gateway implementation (crates/agent-gateway/internal/mesh),
|
|
6
|
-
* the way the ReactorPro desktop implements it. Two halves:
|
|
7
|
-
*
|
|
8
|
-
* A. Pure-JS invariants (always run): fingerprint derivation, signing
|
|
9
|
-
* payload construction, envelope sign/verify round-trip, respond/error
|
|
10
|
-
* shapes, identity minting + tamper detection, settings persistence
|
|
11
|
-
* (the v3.1.3 lesson: a settings block missing from pickBackendSnapshot
|
|
12
|
-
* is silently wiped on save).
|
|
13
|
-
*
|
|
14
|
-
* B. Go cross-check (runs when the ReactorPro checkout + Go are present):
|
|
15
|
-
* the plugin signs an envelope, and the GATEWAY's own VerifyEnvelope
|
|
16
|
-
* must accept it; the gateway signs one, and the plugin must accept it.
|
|
17
|
-
* Skipped cleanly when the checkout is absent — CI without the ReactorPro
|
|
18
|
-
* repo still runs part A.
|
|
19
|
-
*
|
|
20
|
-
* Run: npx tsx plugins/reactorpro-bridge/reactorpro-bridge.extreme.spec.mts
|
|
21
|
-
*/
|
|
22
|
-
import { execFileSync, spawnSync } from 'node:child_process'
|
|
23
|
-
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
24
|
-
import { tmpdir } from 'node:os'
|
|
25
|
-
import { join } from 'node:path'
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
AUTH_KEYS,
|
|
29
|
-
buildManifest,
|
|
30
|
-
buildRespond,
|
|
31
|
-
checkIdentity,
|
|
32
|
-
defaultServeSkills,
|
|
33
|
-
envelope,
|
|
34
|
-
fingerprintFor,
|
|
35
|
-
mintIdentity,
|
|
36
|
-
rawPublicKeyFromPem,
|
|
37
|
-
signEnvelope,
|
|
38
|
-
signingPayload,
|
|
39
|
-
verifyEnvelope,
|
|
40
|
-
} from './reactorproAgent.mjs'
|
|
41
|
-
import { loadOrCreateIdentity, resolveConfig } from './index.mjs'
|
|
42
|
-
|
|
43
|
-
let pass = 0
|
|
44
|
-
let fail = 0
|
|
45
|
-
const failures: string[] = []
|
|
46
|
-
|
|
47
|
-
function ok(cond: unknown, label: string, note = ''): void {
|
|
48
|
-
if (cond) {
|
|
49
|
-
pass++
|
|
50
|
-
console.log(`PASS ${label}`)
|
|
51
|
-
} else {
|
|
52
|
-
fail++
|
|
53
|
-
failures.push(label)
|
|
54
|
-
console.log(`FAIL ${label}${note ? ` — ${note}` : ''}`)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─── A. pure-JS invariants ───────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
// A1. Fingerprint: sha256:<16hex> over "<agent id>\n" + raw SPKI public key.
|
|
61
|
-
{
|
|
62
|
-
const id = mintIdentity('rterm/xcheck/fixture')
|
|
63
|
-
const fp = fingerprintFor('rterm/xcheck/fixture', id.publicKeyPem)
|
|
64
|
-
ok(/^sha256:[0-9a-f]{16}$/.test(fp), 'fingerprint is sha256:<16hex>')
|
|
65
|
-
// The id is inside the digest: same key, different id => different fp.
|
|
66
|
-
const fp2 = fingerprintFor('rterm/xcheck/other', id.publicKeyPem)
|
|
67
|
-
ok(fp !== fp2, 'fingerprint binds the agent id (different id => different fp)')
|
|
68
|
-
// And the raw public key must round-trip from the PEM.
|
|
69
|
-
ok(rawPublicKeyFromPem(id.publicKeyPem).length === 32, 'raw public key is 32 bytes (Ed25519)')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// A2. checkIdentity detects tampering.
|
|
73
|
-
{
|
|
74
|
-
const id = mintIdentity('rterm/xcheck/tamper')
|
|
75
|
-
let threw = false
|
|
76
|
-
try {
|
|
77
|
-
// The identity's id field is `identity` — mutating it (e.g. renaming the
|
|
78
|
-
// agent) breaks the fingerprint, which binds id + key.
|
|
79
|
-
checkIdentity({ ...id, identity: 'someone-else' } as never)
|
|
80
|
-
} catch {
|
|
81
|
-
threw = true
|
|
82
|
-
}
|
|
83
|
-
ok(threw, 'checkIdentity rejects an identity whose fingerprint does not match')
|
|
84
|
-
|
|
85
|
-
let threwKey = false
|
|
86
|
-
const other = mintIdentity('rterm/xcheck/tamper')
|
|
87
|
-
try {
|
|
88
|
-
checkIdentity({ ...id, publicKeyPem: other.publicKeyPem } as never)
|
|
89
|
-
} catch {
|
|
90
|
-
threwKey = true
|
|
91
|
-
}
|
|
92
|
-
ok(threwKey, 'checkIdentity rejects a swapped public key')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// A3. signEnvelope/verifyEnvelope round-trip, ReactorPro convention.
|
|
96
|
-
{
|
|
97
|
-
const id = mintIdentity('rterm/xcheck/signer')
|
|
98
|
-
const cfg = { agentId: 'rterm/xcheck/signer', prefix: 'mesh', fingerprint: id.fingerprint }
|
|
99
|
-
const env = envelope('request', { skill: 'ping', input: {}, reply_to: '_REPLY.abc.123' }, cfg, { to: 'peer-1' })
|
|
100
|
-
const signed = signEnvelope(env, id)
|
|
101
|
-
|
|
102
|
-
ok(AUTH_KEYS.every((k) => typeof (signed as Record<string, unknown>)[k] === 'string'),
|
|
103
|
-
'signed envelope carries sig/pub/fp')
|
|
104
|
-
const v = verifyEnvelope(signed)
|
|
105
|
-
ok(v.ok && v.note !== 'unsigned', `plugin verifies its own signature (${v.note})`)
|
|
106
|
-
|
|
107
|
-
// Tamper with the payload -> signature must fail.
|
|
108
|
-
const tampered = { ...(signed as Record<string, unknown>), payload: { skill: 'invoke', input: { evil: true } } }
|
|
109
|
-
const tv = verifyEnvelope(tampered)
|
|
110
|
-
ok(!tv.ok, 'tampered payload fails verification')
|
|
111
|
-
|
|
112
|
-
// Swap the fingerprint -> identity mismatch.
|
|
113
|
-
const fpSwap = { ...(signed as Record<string, unknown>), fp: 'sha256:deadbeefdeadbeef' }
|
|
114
|
-
const fv = verifyEnvelope(fpSwap)
|
|
115
|
-
ok(!fv.ok, 'swapped fingerprint fails verification')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// A4. The signing payload covers every meaning-bearing field (gateway order).
|
|
119
|
-
{
|
|
120
|
-
const env = {
|
|
121
|
-
v: '0.3.0', id: 'id-1', type: 'respond', ts: '2026-09-17T00:00:00Z',
|
|
122
|
-
from: 'a', to: 'b', task_id: 't-1', in_reply_to: 'r-1', fp: 'sha256:0123456789abcdef',
|
|
123
|
-
trace: { trace_id: 'tr-1', span_id: 'sp-1' },
|
|
124
|
-
error: { code: 3001, message: 'SKILL_NOT_FOUND', retryable: false },
|
|
125
|
-
payload: { output: { x: 1 } },
|
|
126
|
-
}
|
|
127
|
-
const sp = signingPayload(env as never)
|
|
128
|
-
const text = sp.toString('latin1')
|
|
129
|
-
// Length-prefixed fields in gateway order: v, id, type, ts, from, to, task_id, in_reply_to, fp
|
|
130
|
-
ok(text.startsWith('5:0.3.0\n'), 'signing payload starts with the version field, length-prefixed')
|
|
131
|
-
ok(text.includes('4:id-1\n'), 'signing payload includes id')
|
|
132
|
-
ok(text.includes('7:respond\n'), 'signing payload includes type')
|
|
133
|
-
ok(text.includes('1:a\n'), 'signing payload includes from')
|
|
134
|
-
ok(text.includes('5:tr-1\n') || text.includes('4:tr-1\n'), 'signing payload includes trace_id')
|
|
135
|
-
ok(text.includes('4:3001\n'), 'signing payload includes error code')
|
|
136
|
-
// Absent trace/error contribute their empty positions.
|
|
137
|
-
const bare = signingPayload({ v: '0.3.0', id: 'x', type: 'emit', ts: 't', from: 'f' } as never)
|
|
138
|
-
const bareText = bare.toString('latin1')
|
|
139
|
-
ok(bareText.includes('0:\n0:\n'), 'absent trace/error contribute empty positions')
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// A5. buildRespond: output XOR error, in_reply_to + task_id correlation.
|
|
143
|
-
{
|
|
144
|
-
const cfg = { agentId: 'a', prefix: 'mesh' }
|
|
145
|
-
const req = { id: 'req-1', from: 'caller', task_id: 'task-9' }
|
|
146
|
-
const r = buildRespond(req as never, cfg, { output: { pong: true } })
|
|
147
|
-
ok(r.in_reply_to === 'req-1' && r.task_id === 'task-9' && r.to === 'caller',
|
|
148
|
-
'respond correlates in_reply_to/task_id/to')
|
|
149
|
-
ok((r.payload as Record<string, unknown>).output !== undefined, 'respond payload carries output')
|
|
150
|
-
|
|
151
|
-
let threw = false
|
|
152
|
-
try {
|
|
153
|
-
buildRespond(req as never, cfg, { output: { a: 1 }, error: { code: 1, message: 'x', retryable: false } } as never)
|
|
154
|
-
} catch {
|
|
155
|
-
threw = true
|
|
156
|
-
}
|
|
157
|
-
ok(threw, 'respond rejects output AND error together')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// A6. Settings persistence — the v3.1.3 lesson.
|
|
161
|
-
// resolveConfig must read the reactorpro block, and the block must survive
|
|
162
|
-
// the migration snapshot (pickBackendSnapshot) — verified by importing the
|
|
163
|
-
// real migrations module.
|
|
164
|
-
{
|
|
165
|
-
const cfg = resolveConfig({
|
|
166
|
-
settings: {
|
|
167
|
-
reactorpro: {
|
|
168
|
-
enabled: true,
|
|
169
|
-
url: 'nats://mesh.example:4222',
|
|
170
|
-
agentId: 'rterm/xcheck/persist',
|
|
171
|
-
prefix: 'mesh',
|
|
172
|
-
identityPath: '/tmp/xcheck-identity.json',
|
|
173
|
-
dispatchTimeout: 240000,
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
})
|
|
177
|
-
ok(cfg.agentId === 'rterm/xcheck/persist', 'resolveConfig reads reactorpro.agentId')
|
|
178
|
-
ok(cfg.dispatchTimeout === 240000, 'resolveConfig reads reactorpro.dispatchTimeout')
|
|
179
|
-
ok(cfg.enabled === true, 'resolveConfig reads reactorpro.enabled')
|
|
180
|
-
|
|
181
|
-
// The migration whitelist must carry the block. Import the real module.
|
|
182
|
-
const migrationsPath = join(import.meta.dirname, '..', '..', 'packages', 'backend', 'src', 'services', 'settings', 'migrations.ts')
|
|
183
|
-
if (existsSync(migrationsPath)) {
|
|
184
|
-
const src = readFileSync(migrationsPath, 'utf-8')
|
|
185
|
-
ok(src.includes('reactorpro: raw.reactorpro'),
|
|
186
|
-
'pickBackendSnapshot carries reactorpro (v3.1.3 lesson: missing keys are silently wiped)')
|
|
187
|
-
ok(src.includes('normalizeReactorProSettings'),
|
|
188
|
-
'normalizeBackendSettings sanitizes the reactorpro block')
|
|
189
|
-
} else {
|
|
190
|
-
ok(false, 'migrations.ts not found for the persistence check')
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// A7. Identity persistence: mint once, load forever; 0600; tamper detection.
|
|
195
|
-
{
|
|
196
|
-
const dir = mkdtempSync(join(tmpdir(), 'rp-xcheck-'))
|
|
197
|
-
const identityPath = join(dir, 'identity.json')
|
|
198
|
-
const cfg = { agentId: 'rterm/xcheck/persist-id', identityPath }
|
|
199
|
-
const first = loadOrCreateIdentity(cfg as never)
|
|
200
|
-
const second = loadOrCreateIdentity(cfg as never)
|
|
201
|
-
ok(first.fingerprint === second.fingerprint, 'identity is minted once and reloaded unchanged')
|
|
202
|
-
|
|
203
|
-
// Tamper with the stored identity -> loadOrCreateIdentity must throw.
|
|
204
|
-
const stored = JSON.parse(readFileSync(identityPath, 'utf-8'))
|
|
205
|
-
stored.identity = 'attacker'
|
|
206
|
-
writeFileSync(identityPath, JSON.stringify(stored))
|
|
207
|
-
let threw = false
|
|
208
|
-
try {
|
|
209
|
-
loadOrCreateIdentity(cfg as never)
|
|
210
|
-
} catch {
|
|
211
|
-
threw = true
|
|
212
|
-
}
|
|
213
|
-
ok(threw, 'a tampered identity file is rejected on load')
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ─── A2. Manifest shape contract (the gateway's Manifest struct) ─────────────
|
|
217
|
-
// Found live: the plugin sent `identity` where the gateway's Manifest
|
|
218
|
-
// unmarshals `json:"id"`, and bare-string skills where the gateway expects
|
|
219
|
-
// []{id,name,description} objects. manifestMatches drops a manifest whose ID
|
|
220
|
-
// is empty, so the peer was INVISIBLE to gateway discovery. These pin the
|
|
221
|
-
// corrected contract so it cannot regress.
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
const cfg = { agentId: 'rterm/xcheck/manifest', name: 'XCheck', fingerprint: 'sha256:abc123' }
|
|
225
|
-
const m = buildManifest(cfg, ['ping', 'describe', 'status', 'invoke'])
|
|
226
|
-
ok(m.id === 'rterm/xcheck/manifest',
|
|
227
|
-
'manifest uses id (json:"id") — the gateway drops a manifest with empty ID',
|
|
228
|
-
JSON.stringify(Object.keys(m)))
|
|
229
|
-
ok(!('identity' in m), 'manifest must NOT carry a legacy identity key')
|
|
230
|
-
ok(m.name === 'XCheck', 'manifest name is the display name')
|
|
231
|
-
ok(Array.isArray(m.capabilities) && m.capabilities.includes('rterm'),
|
|
232
|
-
'manifest advertises the rterm capability')
|
|
233
|
-
ok(Array.isArray(m.skills) && m.skills.length === 4
|
|
234
|
-
&& m.skills.every((s: { id: string }) => typeof s?.id === 'string'),
|
|
235
|
-
'skills are []{id,...} objects, not bare strings (the gateway Skill struct unmarshals objects)',
|
|
236
|
-
JSON.stringify(m.skills))
|
|
237
|
-
ok(m.fingerprint === 'sha256:abc123', 'manifest carries the fingerprint for TOFU pinning')
|
|
238
|
-
ok(Array.isArray(m.local_agents) && m.local_agents[0]?.id === cfg.agentId,
|
|
239
|
-
'local_agents entries use id too')
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// A string skill list must be normalized to objects without dropping any.
|
|
243
|
-
{
|
|
244
|
-
const m = buildManifest({ agentId: 'a' }, ['ping', 'status'])
|
|
245
|
-
ok(m.skills.length === 2 && m.skills[0].id === 'ping' && m.skills[1].id === 'status',
|
|
246
|
-
'string skill ids normalize to {id,name} objects in order')
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// A discover reply in the GATEWAY's bare-manifest shape must be accepted by
|
|
250
|
-
// the plugin's parser (the old parser only read payload.manifest). The
|
|
251
|
-
// candidates logic lives inline in discoverPeers; this pins the accepted
|
|
252
|
-
// payload shapes by constructing them the way the gateway does.
|
|
253
|
-
{
|
|
254
|
-
const bare = { id: 'gateway/edge-1', name: 'Edge', skills: [{ id: 'ping' }] }
|
|
255
|
-
const agents = { agents: [{ id: 'gateway/edge-2', name: 'Edge2' }] }
|
|
256
|
-
const legacy = { manifest: { id: 'old/peer', name: 'Old' } }
|
|
257
|
-
const shapes = [bare, agents, legacy]
|
|
258
|
-
for (const p of shapes) {
|
|
259
|
-
const candidates = Array.isArray((p as { agents?: unknown[] }).agents)
|
|
260
|
-
? (p as { agents: unknown[] }).agents
|
|
261
|
-
: ((p as { id?: string }).id || (p as { identity?: string }).identity)
|
|
262
|
-
? [p]
|
|
263
|
-
: ((p as { manifest?: { id?: string } }).manifest?.id
|
|
264
|
-
|| (p as { manifest?: { identity?: string } }).manifest?.identity)
|
|
265
|
-
? [(p as { manifest: unknown }).manifest]
|
|
266
|
-
: []
|
|
267
|
-
ok(candidates.length > 0, `discover parser accepts payload shape ${JSON.stringify(Object.keys(p))}`)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ─── A3. Inbound invoke skill (v3.8.8) ───────────────────────────────────────
|
|
272
|
-
// Found live: the manifest ADVERTISED invoke but defaultServeSkills had no
|
|
273
|
-
// handler, so a peer dispatching invoke got SKILL_NOT_FOUND while the
|
|
274
|
-
// manifest promised it. These pin the corrected contract.
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
// A3.1 invoke exists in the default skills and routes to runAgentTask.
|
|
278
|
-
const cfg = { agentId: 'rterm/xcheck/invoke', dispatchTimeout: 180000 }
|
|
279
|
-
const skills = defaultServeSkills(cfg, {
|
|
280
|
-
startedAt: Date.now(),
|
|
281
|
-
runAgentTask: async (prompt: string, opts?: { sessionId?: string; timeoutMs?: number }) => {
|
|
282
|
-
ok(prompt === 'Reply with exactly one word: PONG',
|
|
283
|
-
'invoke passes the prompt through to runAgentTask (arguments.prompt)',
|
|
284
|
-
JSON.stringify({ prompt }))
|
|
285
|
-
ok(typeof opts?.timeoutMs === 'number' && opts.timeoutMs === 170000,
|
|
286
|
-
'invoke stays under the mesh dispatch budget (dispatchTimeout - 10s)',
|
|
287
|
-
JSON.stringify(opts))
|
|
288
|
-
return { ok: true, answer: 'PONG', sessionId: 'mesh-invoke-test' }
|
|
289
|
-
},
|
|
290
|
-
})
|
|
291
|
-
ok(typeof skills.invoke === 'function', 'defaultServeSkills includes an invoke handler')
|
|
292
|
-
|
|
293
|
-
const r1 = await skills.invoke({ arguments: { prompt: 'Reply with exactly one word: PONG' } } as never)
|
|
294
|
-
ok(r1?.output === 'PONG', 'invoke returns the agent answer as output', JSON.stringify(r1))
|
|
295
|
-
ok(r1?.conversation_id === 'mesh-invoke-test', 'invoke returns conversation_id for session persistence')
|
|
296
|
-
|
|
297
|
-
// A3.2 prompt extraction: text/ prompt/ message all work.
|
|
298
|
-
const skills2 = defaultServeSkills({ agentId: 'a' }, {
|
|
299
|
-
runAgentTask: async (prompt: string) => ({ ok: true, answer: `got:${prompt}`, sessionId: 's' }),
|
|
300
|
-
})
|
|
301
|
-
const viaText = await skills2.invoke({ text: 'hello' } as never)
|
|
302
|
-
ok(viaText?.output === 'got:hello', 'invoke extracts input.text')
|
|
303
|
-
const viaPrompt = await skills2.invoke({ prompt: 'hi' } as never)
|
|
304
|
-
ok(viaPrompt?.output === 'got:hi', 'invoke extracts input.prompt')
|
|
305
|
-
const viaString = await skills2.invoke('plain' as never)
|
|
306
|
-
ok(viaString?.output === 'got:plain', 'invoke extracts a bare string input')
|
|
307
|
-
|
|
308
|
-
// A3.3 conversation_id routes the follow-up to the same agent session.
|
|
309
|
-
const seenSessions: Array<string | undefined> = []
|
|
310
|
-
const skills3 = defaultServeSkills({ agentId: 'a' }, {
|
|
311
|
-
runAgentTask: async (_p: string, o?: { sessionId?: string }) => {
|
|
312
|
-
seenSessions.push(o?.sessionId)
|
|
313
|
-
return { ok: true, answer: 'x', sessionId: o?.sessionId ?? 'new' }
|
|
314
|
-
},
|
|
315
|
-
})
|
|
316
|
-
const first = await skills3.invoke({ text: 'one' } as never)
|
|
317
|
-
// First call: no conversation_id yet -> the hook generates a session.
|
|
318
|
-
ok(seenSessions[0] === undefined, 'first invoke has no session yet (the hook mints one)')
|
|
319
|
-
await skills3.invoke({ text: 'two', conversation_id: first.conversation_id } as never)
|
|
320
|
-
ok(seenSessions[1] === first.conversation_id && first.conversation_id !== undefined,
|
|
321
|
-
'conversation_id reuses the same agent session on the follow-up',
|
|
322
|
-
JSON.stringify({ seenSessions, firstId: first.conversation_id }))
|
|
323
|
-
|
|
324
|
-
// A3.4 no runAgentTask hook -> a CLEAR error, not SKILL_NOT_FOUND.
|
|
325
|
-
const skills4 = defaultServeSkills({ agentId: 'a' }, {})
|
|
326
|
-
const r4 = await skills4.invoke({ text: 'x' } as never)
|
|
327
|
-
ok(r4?.error?.code === 3002 && String(r4?.error?.message).includes('INVOKE_UNAVAILABLE'),
|
|
328
|
-
'invoke without the runAgentTask hook returns INVOKE_UNAVAILABLE (not SKILL_NOT_FOUND)',
|
|
329
|
-
JSON.stringify(r4))
|
|
330
|
-
|
|
331
|
-
// A3.5 empty prompt -> a clear error.
|
|
332
|
-
const skills5 = defaultServeSkills({ agentId: 'a' }, {
|
|
333
|
-
runAgentTask: async () => ({ ok: true, answer: 'should not run', sessionId: 's' }),
|
|
334
|
-
})
|
|
335
|
-
const r5 = await skills5.invoke({} as never)
|
|
336
|
-
ok(r5?.error?.code === 3003 && String(r5?.error?.message).includes('INVOKE_NEEDS_PROMPT'),
|
|
337
|
-
'invoke with no prompt returns INVOKE_NEEDS_PROMPT without running the agent',
|
|
338
|
-
JSON.stringify(r5))
|
|
339
|
-
|
|
340
|
-
// A3.6 a failed agent turn -> a retryable error carrying the reason.
|
|
341
|
-
const skills6 = defaultServeSkills({ agentId: 'a' }, {
|
|
342
|
-
runAgentTask: async () => ({ ok: false, answer: '', error: 'model offline', sessionId: 's' }),
|
|
343
|
-
})
|
|
344
|
-
const r6 = await skills6.invoke({ text: 'x' } as never)
|
|
345
|
-
ok(r6?.error?.code === 5001 && String(r6?.error?.message).includes('model offline'),
|
|
346
|
-
'a failed agent turn surfaces as a retryable error with the reason',
|
|
347
|
-
JSON.stringify(r6))
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ─── B. Go cross-check against the REAL gateway ──────────────────────────────
|
|
351
|
-
|
|
352
|
-
const RP_CHECKOUT = process.env.RP_CHECKOUT ?? join(process.env.HOME ?? '', 'work', 'ReactorPro')
|
|
353
|
-
const RP_GW = join(RP_CHECKOUT, 'crates', 'agent-gateway')
|
|
354
|
-
|
|
355
|
-
if (existsSync(join(RP_GW, 'go.mod')) && existsSync('/opt/homebrew/bin/go')) {
|
|
356
|
-
console.log('\n-- Go cross-check against the real gateway --')
|
|
357
|
-
const fixtureDir = mkdtempSync(join(tmpdir(), 'rp-fixtures-'))
|
|
358
|
-
|
|
359
|
-
// B1. Plugin signs -> gateway verifies.
|
|
360
|
-
const id = mintIdentity('rterm/xcheck/go')
|
|
361
|
-
writeFileSync(join(fixtureDir, 'gateway_identity.json'), JSON.stringify({
|
|
362
|
-
identity: 'rterm/xcheck/go',
|
|
363
|
-
privateKeyPem: id.privateKeyPem,
|
|
364
|
-
publicKeyPem: id.publicKeyPem,
|
|
365
|
-
fingerprint: id.fingerprint,
|
|
366
|
-
}, null, 2))
|
|
367
|
-
|
|
368
|
-
const cfg = { agentId: 'rterm/xcheck/go', prefix: 'mesh', fingerprint: id.fingerprint }
|
|
369
|
-
const env = envelope('respond', { output: { cross: true } }, cfg, { to: 'reactorpro/edge-1' })
|
|
370
|
-
const signed = signEnvelope(env, id)
|
|
371
|
-
// COMPACT on purpose. The gateway's Envelope.Payload is a json.RawMessage,
|
|
372
|
-
// which captures the payload bytes EXACTLY as they appear in the file —
|
|
373
|
-
// including any pretty-print indentation. The plugin signs
|
|
374
|
-
// JSON.stringify(payload) (compact) and publishes j(signed) (compact), so a
|
|
375
|
-
// pretty-printed fixture would make the gateway re-hash DIFFERENT bytes than
|
|
376
|
-
// were signed and fail verification even though the real wire exchange is
|
|
377
|
-
// correct. The fixture must be byte-faithful to the wire.
|
|
378
|
-
writeFileSync(join(fixtureDir, 'plugin_signed_envelope.json'), JSON.stringify(signed))
|
|
379
|
-
writeFileSync(join(fixtureDir, 'plugin_signing_payload.hex'),
|
|
380
|
-
signingPayload(signed as never).toString('hex') + '\n')
|
|
381
|
-
|
|
382
|
-
const goTest = spawnSync('/opt/homebrew/bin/go', ['test', './internal/mesh', '-run', 'TestRtermBridgeCrossVerify', '-v', '-count=1'],
|
|
383
|
-
{ cwd: RP_GW, env: { ...process.env, RTERM_BRIDGE_FIXTURES: fixtureDir }, encoding: 'utf-8', timeout: 180000 })
|
|
384
|
-
|
|
385
|
-
ok(goTest.status === 0,
|
|
386
|
-
`the REAL gateway verifies the plugin's envelope (go test exit ${goTest.status})`,
|
|
387
|
-
goTest.status !== 0 ? (goTest.stderr || goTest.stdout || '').split('\n').filter((l) => l.includes('FAIL') || l.includes('mismatch')).slice(0, 3).join('; ') : '')
|
|
388
|
-
|
|
389
|
-
// B2. Gateway signs -> plugin verifies.
|
|
390
|
-
const gwEnvPath = join(fixtureDir, 'gateway_signed_envelope.json')
|
|
391
|
-
if (existsSync(gwEnvPath)) {
|
|
392
|
-
const gwEnv = JSON.parse(readFileSync(gwEnvPath, 'utf-8'))
|
|
393
|
-
const v = verifyEnvelope(gwEnv)
|
|
394
|
-
ok(v.ok && v.note !== 'unsigned',
|
|
395
|
-
`the plugin verifies the gateway's envelope (${v.note})`,
|
|
396
|
-
JSON.stringify(v))
|
|
397
|
-
} else {
|
|
398
|
-
ok(false, 'gateway_signed_envelope.json was not produced by the Go test')
|
|
399
|
-
}
|
|
400
|
-
} else {
|
|
401
|
-
console.log('\n-- Go cross-check skipped (ReactorPro checkout or Go not present) --')
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
console.log(`\n${pass} passed, ${fail} failed`)
|
|
405
|
-
if (fail > 0) {
|
|
406
|
-
console.log(`FAILURES:\n${failures.map((f) => ` - ${f}`).join('\n')}`)
|
|
407
|
-
process.exit(1)
|
|
408
|
-
}
|
|
409
|
-
console.log('reactorpro-bridge: ALL TESTS PASSED')
|