@aiassesstech/fleet-bus 0.1.5 → 0.1.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/package.json +2 -1
- package/scripts/vps-smoke-test.mjs +335 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiassesstech/fleet-bus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Fleet communication infrastructure for AI Assess Tech governance agents — typed messaging, routing rules, audit trail, Agent Cards.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
|
+
"scripts",
|
|
10
11
|
"README.md",
|
|
11
12
|
"LICENSE"
|
|
12
13
|
],
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fleet-Bus VPS Smoke Test (v2 — fixed for current API)
|
|
4
|
+
*
|
|
5
|
+
* Run from any extension directory on the VPS:
|
|
6
|
+
* cd /root/.openclaw/extensions/grillo
|
|
7
|
+
* node node_modules/@aiassesstech/fleet-bus/scripts/vps-smoke-test.mjs
|
|
8
|
+
*
|
|
9
|
+
* Or copy to /tmp and run:
|
|
10
|
+
* cd /root/.openclaw/extensions/grillo && node /tmp/vps-smoke-test.mjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
enforceRouting,
|
|
15
|
+
createFleetMessage,
|
|
16
|
+
serializeForTransport,
|
|
17
|
+
deserializeFromTransport,
|
|
18
|
+
extractFleetMessage,
|
|
19
|
+
isFleetMessage,
|
|
20
|
+
AgentCardRegistry,
|
|
21
|
+
AuditTrailWriter,
|
|
22
|
+
verifyChain,
|
|
23
|
+
ROUTING_RULES,
|
|
24
|
+
GRILLO_CARD,
|
|
25
|
+
JESSIE_CARD,
|
|
26
|
+
NOLE_CARD,
|
|
27
|
+
NOAH_CARD,
|
|
28
|
+
MIGHTY_MARK_CARD,
|
|
29
|
+
SAM_CARD,
|
|
30
|
+
} from '@aiassesstech/fleet-bus';
|
|
31
|
+
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import * as os from 'node:os';
|
|
35
|
+
|
|
36
|
+
let passed = 0;
|
|
37
|
+
let failed = 0;
|
|
38
|
+
|
|
39
|
+
function test(name, fn) {
|
|
40
|
+
try {
|
|
41
|
+
fn();
|
|
42
|
+
passed++;
|
|
43
|
+
console.log(` ✓ ${name}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
failed++;
|
|
46
|
+
console.log(` ✗ ${name}`);
|
|
47
|
+
console.log(` ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assert(condition, msg) {
|
|
52
|
+
if (!condition) throw new Error(msg || 'Assertion failed');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function routingAllowed(from, to, method) {
|
|
56
|
+
const msg = createFleetMessage(from, to, method, {});
|
|
57
|
+
return enforceRouting(msg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Test Suite 1: Routing Rules ──────────────────────────────────
|
|
61
|
+
console.log('\n╔══════════════════════════════════════════════╗');
|
|
62
|
+
console.log('║ Fleet-Bus VPS Smoke Tests (v2) ║');
|
|
63
|
+
console.log('╚══════════════════════════════════════════════╝\n');
|
|
64
|
+
|
|
65
|
+
console.log('1. Routing Rules');
|
|
66
|
+
|
|
67
|
+
test('Grillo → Jessie (assessment/result) — allowed', () => {
|
|
68
|
+
const result = routingAllowed('grillo', 'jessie', 'assessment/result');
|
|
69
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Grillo → Nole (assessment/result) — blocked (unidirectional to Commander only)', () => {
|
|
73
|
+
const result = routingAllowed('grillo', 'nole', 'assessment/result');
|
|
74
|
+
assert(result.allowed === false, 'Should be blocked');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('Jessie → Grillo (fleet/ping) — allowed (Commander unrestricted + Grillo accepts pings)', () => {
|
|
78
|
+
const result = routingAllowed('jessie', 'grillo', 'fleet/ping');
|
|
79
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('Jessie → Sam (task/assign) — allowed', () => {
|
|
83
|
+
const result = routingAllowed('jessie', 'sam', 'task/assign');
|
|
84
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Nole → Jessie (proposal/submit) — allowed', () => {
|
|
88
|
+
const result = routingAllowed('nole', 'jessie', 'proposal/submit');
|
|
89
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Nole → Noah (proposal/submit) — blocked (governed, Commander only)', () => {
|
|
93
|
+
const result = routingAllowed('nole', 'noah', 'proposal/submit');
|
|
94
|
+
assert(result.allowed === false, 'Should be blocked');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('Sam → Jessie (task/status) — allowed', () => {
|
|
98
|
+
const result = routingAllowed('sam', 'jessie', 'task/status');
|
|
99
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Sam → Grillo (task/status) — blocked (Grillo only accepts fleet/*)', () => {
|
|
103
|
+
const result = routingAllowed('sam', 'grillo', 'task/status');
|
|
104
|
+
assert(result.allowed === false, 'Should be blocked');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Noah → Jessie (drift/alert) — allowed', () => {
|
|
108
|
+
const result = routingAllowed('noah', 'jessie', 'drift/alert');
|
|
109
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('Mark → Jessie (health/alert) — allowed', () => {
|
|
113
|
+
const result = routingAllowed('mighty-mark', 'jessie', 'health/alert');
|
|
114
|
+
assert(result.allowed === true, `Expected allowed, got: ${JSON.stringify(result)}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('Grillo → Sam (assessment/result) — blocked', () => {
|
|
118
|
+
const result = routingAllowed('grillo', 'sam', 'assessment/result');
|
|
119
|
+
assert(result.allowed === false, 'Should be blocked');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Test Suite 2: Message Creation & Serialization ────────────────
|
|
123
|
+
console.log('\n2. Message Creation & Serialization');
|
|
124
|
+
|
|
125
|
+
test('Create a FleetMessage with all required fields', () => {
|
|
126
|
+
const msg = createFleetMessage('grillo', 'jessie', 'assessment/result', { score: 85 });
|
|
127
|
+
assert(msg.from === 'grillo', 'from should be grillo');
|
|
128
|
+
assert(msg.to === 'jessie', 'to should be jessie');
|
|
129
|
+
assert(msg.method === 'assessment/result', 'method should be assessment/result');
|
|
130
|
+
assert(msg.params.score === 85, 'params should include score');
|
|
131
|
+
assert(typeof msg.id === 'string' && msg.id.length > 0, 'should have an id');
|
|
132
|
+
assert(typeof msg.timestamp === 'string', 'should have timestamp');
|
|
133
|
+
assert(typeof msg.nonce === 'string', 'should have nonce');
|
|
134
|
+
assert(msg.version === '1.0', 'version should be 1.0');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('Serialize and deserialize preserves message integrity', () => {
|
|
138
|
+
const original = createFleetMessage('noah', 'jessie', 'drift/alert', { tdi: 0.73, trend: 'rising' });
|
|
139
|
+
const serialized = serializeForTransport(original);
|
|
140
|
+
assert(serialized.startsWith('%%FLEET%%'), 'Should have %%FLEET%% prefix');
|
|
141
|
+
const deserialized = deserializeFromTransport(serialized);
|
|
142
|
+
assert(deserialized.from === 'noah', 'from preserved');
|
|
143
|
+
assert(deserialized.to === 'jessie', 'to preserved');
|
|
144
|
+
assert(deserialized.method === 'drift/alert', 'method preserved');
|
|
145
|
+
assert(deserialized.params.tdi === 0.73, 'params preserved');
|
|
146
|
+
assert(deserialized.id === original.id, 'id preserved');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('isFleetMessage detects %%FLEET%% prefix', () => {
|
|
150
|
+
assert(isFleetMessage('%%FLEET%%{"id":"x"}') === true, 'Should detect fleet message');
|
|
151
|
+
assert(isFleetMessage('Hello world') === false, 'Should not detect normal message');
|
|
152
|
+
assert(isFleetMessage('') === false, 'Should not detect empty string');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('extractFleetMessage parses embedded fleet messages', () => {
|
|
156
|
+
const msg = createFleetMessage('grillo', 'jessie', 'fleet/ping', {});
|
|
157
|
+
const serialized = serializeForTransport(msg);
|
|
158
|
+
const extracted = extractFleetMessage({ content: serialized });
|
|
159
|
+
assert(extracted !== null, 'Should extract message');
|
|
160
|
+
assert(extracted.from === 'grillo', 'from should be grillo');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('Deserialize rejects malformed messages', () => {
|
|
164
|
+
assert(deserializeFromTransport('%%FLEET%%{invalid json') === null, 'Should reject bad JSON');
|
|
165
|
+
assert(deserializeFromTransport('%%FLEET%%{"id":"x"}') === null, 'Should reject missing fields');
|
|
166
|
+
assert(deserializeFromTransport('not a fleet message') === null, 'Should reject non-fleet');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── Test Suite 3: Agent Cards (Live Data) ────────────────────────
|
|
170
|
+
console.log('\n3. Agent Cards (Live from disk)');
|
|
171
|
+
|
|
172
|
+
const cardsDir = path.join(os.homedir(), '.openclaw', 'workspace', 'fleet-bus', 'cards');
|
|
173
|
+
|
|
174
|
+
test('Cards directory exists', () => {
|
|
175
|
+
assert(fs.existsSync(cardsDir), `Cards dir not found at ${cardsDir}`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const expectedCards = ['grillo', 'noah', 'nole', 'mighty-mark', 'jessie', 'sam'];
|
|
179
|
+
|
|
180
|
+
test(`All ${expectedCards.length} agent cards exist on disk`, () => {
|
|
181
|
+
for (const agent of expectedCards) {
|
|
182
|
+
assert(fs.existsSync(path.join(cardsDir, `${agent}.json`)), `Missing: ${agent}.json`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
for (const agent of expectedCards) {
|
|
187
|
+
const cardPath = path.join(cardsDir, `${agent}.json`);
|
|
188
|
+
if (!fs.existsSync(cardPath)) continue;
|
|
189
|
+
|
|
190
|
+
test(`Card ${agent} has valid structure`, () => {
|
|
191
|
+
const card = JSON.parse(fs.readFileSync(cardPath, 'utf-8'));
|
|
192
|
+
assert(card.agentId === agent, `agentId should be ${agent}`);
|
|
193
|
+
assert(typeof card.role === 'string', 'role should be string');
|
|
194
|
+
assert(typeof card.status === 'string', 'status should be string');
|
|
195
|
+
assert(typeof card.authLevel === 'string', 'authLevel should be string');
|
|
196
|
+
assert(typeof card.communicationMode === 'string', 'communicationMode should be string');
|
|
197
|
+
assert(Array.isArray(card.supportedMethods), 'supportedMethods should be array');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Test Suite 4: Audit Trail ────────────────────────────────────
|
|
202
|
+
console.log('\n4. Audit Trail (Hash Chain)');
|
|
203
|
+
|
|
204
|
+
const testAuditDir = path.join(os.tmpdir(), `fleet-bus-smoke-${Date.now()}`);
|
|
205
|
+
|
|
206
|
+
test('Write audit events and verify hash chain', () => {
|
|
207
|
+
const writer = new AuditTrailWriter({ agentId: 'grillo', auditDir: testAuditDir });
|
|
208
|
+
|
|
209
|
+
writer.record({
|
|
210
|
+
eventId: 'test-1',
|
|
211
|
+
eventType: 'message_sent',
|
|
212
|
+
outcome: 'delivered',
|
|
213
|
+
message: createFleetMessage('grillo', 'jessie', 'assessment/result', { score: 85 }),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
writer.record({
|
|
217
|
+
eventId: 'test-2',
|
|
218
|
+
eventType: 'message_sent',
|
|
219
|
+
outcome: 'delivered',
|
|
220
|
+
message: createFleetMessage('grillo', 'jessie', 'fleet/ping', {}),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
writer.record({
|
|
224
|
+
eventId: 'test-3',
|
|
225
|
+
eventType: 'routing_violation',
|
|
226
|
+
outcome: 'rejected',
|
|
227
|
+
details: 'unidirectional constraint',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const auditFile = path.join(testAuditDir, 'grillo.audit.jsonl');
|
|
231
|
+
assert(fs.existsSync(auditFile), 'Audit JSONL file should exist');
|
|
232
|
+
|
|
233
|
+
const lines = fs.readFileSync(auditFile, 'utf-8').trim().split('\n');
|
|
234
|
+
assert(lines.length === 3, `Expected 3 audit events, got ${lines.length}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('Verify hash chain integrity', () => {
|
|
238
|
+
const writer = new AuditTrailWriter({ agentId: 'grillo', auditDir: testAuditDir });
|
|
239
|
+
const { verification } = writer.readAndVerify();
|
|
240
|
+
assert(verification.valid === true, `Chain should be valid, got: ${JSON.stringify(verification)}`);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('Tampered audit is detected', () => {
|
|
244
|
+
const auditFile = path.join(testAuditDir, 'grillo.audit.jsonl');
|
|
245
|
+
const lines = fs.readFileSync(auditFile, 'utf-8').trim().split('\n');
|
|
246
|
+
const event = JSON.parse(lines[1]);
|
|
247
|
+
event.outcome = 'TAMPERED';
|
|
248
|
+
lines[1] = JSON.stringify(event);
|
|
249
|
+
fs.writeFileSync(auditFile, lines.join('\n') + '\n');
|
|
250
|
+
|
|
251
|
+
const allEvents = lines.map(l => JSON.parse(l));
|
|
252
|
+
const result = verifyChain(allEvents);
|
|
253
|
+
assert(result.valid === false, 'Tampered chain should fail verification');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── Test Suite 5: Pre-defined Cards ──────────────────────────────
|
|
257
|
+
console.log('\n5. Pre-defined Agent Cards');
|
|
258
|
+
|
|
259
|
+
test('GRILLO_CARD has correct identity', () => {
|
|
260
|
+
assert(GRILLO_CARD.agentId === 'grillo');
|
|
261
|
+
assert(GRILLO_CARD.role === 'conscience');
|
|
262
|
+
assert(GRILLO_CARD.communicationMode === 'unidirectional');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('JESSIE_CARD has commander role', () => {
|
|
266
|
+
assert(JESSIE_CARD.agentId === 'jessie');
|
|
267
|
+
assert(JESSIE_CARD.role === 'commander');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('NOLE_CARD has governed auth', () => {
|
|
271
|
+
assert(NOLE_CARD.agentId === 'nole');
|
|
272
|
+
assert(NOLE_CARD.authLevel === 'governed');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('NOAH_CARD has read-only auth', () => {
|
|
276
|
+
assert(NOAH_CARD.agentId === 'noah');
|
|
277
|
+
assert(NOAH_CARD.authLevel === 'read-only');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('SAM_CARD has sandboxed auth', () => {
|
|
281
|
+
assert(SAM_CARD.agentId === 'sam');
|
|
282
|
+
assert(SAM_CARD.authLevel === 'sandboxed');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('MIGHTY_MARK_CARD has security-elevated auth', () => {
|
|
286
|
+
assert(MIGHTY_MARK_CARD.agentId === 'mighty-mark');
|
|
287
|
+
assert(MIGHTY_MARK_CARD.authLevel === 'security-elevated');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('ROUTING_RULES covers all 6 agents', () => {
|
|
291
|
+
const agents = ['jessie', 'grillo', 'noah', 'nole', 'sam', 'mighty-mark'];
|
|
292
|
+
for (const a of agents) {
|
|
293
|
+
const rule = ROUTING_RULES.find(r => r.agent === a);
|
|
294
|
+
assert(rule, `Missing routing rule for ${a}`);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── Test Suite 6: Live Audit Trail Verification ──────────────────
|
|
299
|
+
console.log('\n6. Live Audit Trail (from disk)');
|
|
300
|
+
|
|
301
|
+
const liveAuditDir = path.join(os.homedir(), '.openclaw', 'workspace', 'fleet-bus', 'audit');
|
|
302
|
+
|
|
303
|
+
if (fs.existsSync(liveAuditDir)) {
|
|
304
|
+
const auditFiles = fs.readdirSync(liveAuditDir).filter(f => f.endsWith('.audit.jsonl'));
|
|
305
|
+
|
|
306
|
+
test(`Found ${auditFiles.length} audit trail file(s)`, () => {
|
|
307
|
+
assert(auditFiles.length > 0, 'Should have at least one audit file');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
for (const file of auditFiles) {
|
|
311
|
+
const agentId = file.replace('.audit.jsonl', '');
|
|
312
|
+
const filePath = path.join(liveAuditDir, file);
|
|
313
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
314
|
+
if (!content) continue;
|
|
315
|
+
|
|
316
|
+
const events = content.split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
|
317
|
+
|
|
318
|
+
test(`Audit chain: ${agentId} (${events.length} events)`, () => {
|
|
319
|
+
const result = verifyChain(events);
|
|
320
|
+
assert(result.valid === true, `Chain broken: ${JSON.stringify(result)}`);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
console.log(' (No live audit directory found — skipping)');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Cleanup ──────────────────────────────────────────────────────
|
|
328
|
+
fs.rmSync(testAuditDir, { recursive: true, force: true });
|
|
329
|
+
|
|
330
|
+
// ── Results ──────────────────────────────────────────────────────
|
|
331
|
+
console.log('\n══════════════════════════════════════════════');
|
|
332
|
+
console.log(` Results: ${passed} passed, ${failed} failed`);
|
|
333
|
+
console.log('══════════════════════════════════════════════\n');
|
|
334
|
+
|
|
335
|
+
process.exit(failed > 0 ? 1 : 0);
|