@aiassesstech/mighty-mark 0.5.4 → 0.5.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/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to `@aiassesstech/mighty-mark` will be documented in this fi
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.5] — 2026-03-08
9
+
10
+ **Fleet-bus diagnostics script + updated VPS smoke test.**
11
+
12
+ ### Added
13
+ - `fleet-bus-diagnostics.sh` — command-line fleet-bus health checker (cards, audit chain, message flow, gateway init, live ping test).
14
+ - Supports `--live` (gateway ping test), `--verbose` (detailed event listing).
15
+
16
+ ### Fixed
17
+ - Updated `vps-smoke-test.mjs` to use current `enforceRouting(FleetMessage)` API (was using outdated 3-arg signature).
18
+ - Added all 6 agent cards to smoke test (was only checking 4).
19
+ - Fixed `AuditTrailWriter` usage to use `.record()` instead of `.write()`.
20
+
8
21
  ## [0.5.4] — 2026-03-07
9
22
 
10
23
  **Documentation sync — updated check counts, rules, guides, and deployment playbook.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiassesstech/mighty-mark",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "System Health Sentinel for AI Assess Tech Fleet — autonomous monitoring, watchdog recovery, and fleet infrastructure oversight.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ##############################################################################
5
+ # Fleet-Bus Diagnostics — Command-Line Health & Connectivity Checks
6
+ #
7
+ # Run from the VPS:
8
+ # bash /opt/mighty-mark/fleet-bus-diagnostics.sh
9
+ # bash /opt/mighty-mark/fleet-bus-diagnostics.sh --live # includes live ping test
10
+ # bash /opt/mighty-mark/fleet-bus-diagnostics.sh --verbose # show audit event details
11
+ #
12
+ # What it checks:
13
+ # 1. Agent cards on disk (structure, staleness)
14
+ # 2. Audit trail integrity (SHA-256 hash chain per agent)
15
+ # 3. Recent message flow (who sent what to whom)
16
+ # 4. Gateway fleet-bus initialization logs
17
+ # 5. (--live) Send a fleet/ping via gateway and check for response
18
+ ##############################################################################
19
+
20
+ OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
21
+ FLEET_BUS_DIR="$OPENCLAW_HOME/workspace/fleet-bus"
22
+ CARDS_DIR="$FLEET_BUS_DIR/cards"
23
+ AUDIT_DIR="$FLEET_BUS_DIR/audit"
24
+ AGENTS=("jessie" "grillo" "noah" "nole" "sam" "mighty-mark")
25
+ LIVE_TEST=false
26
+ VERBOSE=false
27
+
28
+ for arg in "$@"; do
29
+ case "$arg" in
30
+ --live) LIVE_TEST=true ;;
31
+ --verbose) VERBOSE=true ;;
32
+ --help|-h)
33
+ echo "Usage: $0 [--live] [--verbose]"
34
+ echo " --live Send a live fleet/ping through the gateway"
35
+ echo " --verbose Show detailed audit event data"
36
+ exit 0
37
+ ;;
38
+ esac
39
+ done
40
+
41
+ PASS=0
42
+ WARN=0
43
+ FAIL=0
44
+
45
+ check() {
46
+ local label=$1 status=$2 detail=$3
47
+ case "$status" in
48
+ PASS) echo " ✅ $label — $detail"; PASS=$((PASS + 1)) ;;
49
+ WARN) echo " ⚠️ $label — $detail"; WARN=$((WARN + 1)) ;;
50
+ FAIL) echo " ❌ $label — $detail"; FAIL=$((FAIL + 1)) ;;
51
+ esac
52
+ }
53
+
54
+ echo ""
55
+ echo "╔══════════════════════════════════════════════╗"
56
+ echo "║ Fleet-Bus Diagnostics ║"
57
+ echo "╚══════════════════════════════════════════════╝"
58
+ echo ""
59
+ echo " Fleet-bus dir: $FLEET_BUS_DIR"
60
+ echo " Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
61
+ echo ""
62
+
63
+ ###############################################################################
64
+ # 1. Fleet-Bus Directory Structure
65
+ ###############################################################################
66
+ echo "=== 1. Directory Structure ==="
67
+
68
+ if [ -d "$FLEET_BUS_DIR" ]; then
69
+ check "Fleet-bus directory" "PASS" "$FLEET_BUS_DIR exists"
70
+ else
71
+ check "Fleet-bus directory" "FAIL" "$FLEET_BUS_DIR not found"
72
+ echo ""
73
+ echo " Fleet-bus directory missing. Is the gateway running?"
74
+ echo " Try: systemctl restart openclaw-gateway"
75
+ exit 1
76
+ fi
77
+
78
+ if [ -d "$CARDS_DIR" ]; then
79
+ CARD_COUNT=$(ls -1 "$CARDS_DIR"/*.json 2>/dev/null | wc -l | tr -d ' ')
80
+ check "Cards directory" "PASS" "$CARDS_DIR — $CARD_COUNT card file(s)"
81
+ else
82
+ check "Cards directory" "FAIL" "$CARDS_DIR not found"
83
+ fi
84
+
85
+ if [ -d "$AUDIT_DIR" ]; then
86
+ AUDIT_COUNT=$(ls -1 "$AUDIT_DIR"/*.audit.jsonl 2>/dev/null | wc -l | tr -d ' ')
87
+ check "Audit directory" "PASS" "$AUDIT_DIR — $AUDIT_COUNT audit file(s)"
88
+ else
89
+ check "Audit directory" "WARN" "$AUDIT_DIR not found — no audit trail yet"
90
+ fi
91
+ echo ""
92
+
93
+ ###############################################################################
94
+ # 2. Agent Cards
95
+ ###############################################################################
96
+ echo "=== 2. Agent Cards ==="
97
+
98
+ NOW_EPOCH=$(date +%s)
99
+
100
+ for AGENT in "${AGENTS[@]}"; do
101
+ CARD_FILE="$CARDS_DIR/${AGENT}.json"
102
+ if [ ! -f "$CARD_FILE" ]; then
103
+ check "Card: $AGENT" "FAIL" "not found at $CARD_FILE"
104
+ continue
105
+ fi
106
+
107
+ CARD_DATA=$(python3 -c "
108
+ import json, sys
109
+ with open('$CARD_FILE') as f:
110
+ c = json.load(f)
111
+ role = c.get('role', '?')
112
+ status = c.get('status', '?')
113
+ auth = c.get('authLevel', '?')
114
+ mode = c.get('communicationMode', '?')
115
+ last_seen = c.get('lastSeen', '')
116
+ methods = len(c.get('supportedMethods', []))
117
+ print(f'{role}|{status}|{auth}|{mode}|{last_seen}|{methods}')
118
+ " 2>/dev/null || echo "ERROR")
119
+
120
+ if [ "$CARD_DATA" = "ERROR" ]; then
121
+ check "Card: $AGENT" "FAIL" "could not parse JSON"
122
+ continue
123
+ fi
124
+
125
+ IFS='|' read -r ROLE STATUS AUTH MODE LAST_SEEN METHODS <<< "$CARD_DATA"
126
+
127
+ STALE=""
128
+ if [ -n "$LAST_SEEN" ]; then
129
+ SEEN_EPOCH=$(date -d "$LAST_SEEN" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${LAST_SEEN%%.*}" +%s 2>/dev/null || echo "0")
130
+ if [ "$SEEN_EPOCH" -gt 0 ]; then
131
+ AGE_HOURS=$(( (NOW_EPOCH - SEEN_EPOCH) / 3600 ))
132
+ if [ "$AGE_HOURS" -gt 48 ]; then
133
+ STALE=" ⚠️ stale (${AGE_HOURS}h ago)"
134
+ fi
135
+ fi
136
+ fi
137
+
138
+ check "Card: $AGENT" "PASS" "role=$ROLE status=$STATUS auth=$AUTH mode=$MODE methods=$METHODS${STALE}"
139
+ done
140
+ echo ""
141
+
142
+ ###############################################################################
143
+ # 3. Audit Trail Integrity
144
+ ###############################################################################
145
+ echo "=== 3. Audit Trail ==="
146
+
147
+ TOTAL_EVENTS=0
148
+ declare -A AGENT_EVENTS
149
+
150
+ for AGENT in "${AGENTS[@]}"; do
151
+ AUDIT_FILE="$AUDIT_DIR/${AGENT}.audit.jsonl"
152
+ if [ ! -f "$AUDIT_FILE" ]; then
153
+ check "Audit: $AGENT" "WARN" "no audit file yet"
154
+ AGENT_EVENTS[$AGENT]=0
155
+ continue
156
+ fi
157
+
158
+ EVENT_COUNT=$(wc -l < "$AUDIT_FILE" | tr -d ' ')
159
+ TOTAL_EVENTS=$((TOTAL_EVENTS + EVENT_COUNT))
160
+ AGENT_EVENTS[$AGENT]=$EVENT_COUNT
161
+
162
+ CHAIN_FILE="$AUDIT_DIR/${AGENT}.chain.json"
163
+ if [ -f "$CHAIN_FILE" ]; then
164
+ CHAIN_COUNT=$(python3 -c "
165
+ import json
166
+ with open('$CHAIN_FILE') as f:
167
+ c = json.load(f)
168
+ print(c.get('eventCount', 0))
169
+ " 2>/dev/null || echo "?")
170
+ if [ "$CHAIN_COUNT" = "$EVENT_COUNT" ]; then
171
+ check "Audit: $AGENT" "PASS" "$EVENT_COUNT events, chain state consistent"
172
+ else
173
+ check "Audit: $AGENT" "WARN" "$EVENT_COUNT events in JSONL but chain says $CHAIN_COUNT"
174
+ fi
175
+ else
176
+ check "Audit: $AGENT" "WARN" "$EVENT_COUNT events but no chain state file"
177
+ fi
178
+ done
179
+
180
+ echo ""
181
+ echo " Total audit events across fleet: $TOTAL_EVENTS"
182
+ echo ""
183
+
184
+ ###############################################################################
185
+ # 4. Recent Message Flow
186
+ ###############################################################################
187
+ echo "=== 4. Message Flow (Last 50 Events) ==="
188
+
189
+ HAS_MESSAGES=false
190
+
191
+ for AGENT in "${AGENTS[@]}"; do
192
+ AUDIT_FILE="$AUDIT_DIR/${AGENT}.audit.jsonl"
193
+ [ ! -f "$AUDIT_FILE" ] && continue
194
+ [ "${AGENT_EVENTS[$AGENT]}" = "0" ] && continue
195
+
196
+ FLOW=$(python3 -c "
197
+ import json, sys
198
+
199
+ events = []
200
+ with open('$AUDIT_FILE') as f:
201
+ for line in f:
202
+ line = line.strip()
203
+ if not line:
204
+ continue
205
+ try:
206
+ events.append(json.loads(line))
207
+ except:
208
+ pass
209
+
210
+ recent = events[-50:] if len(events) > 50 else events
211
+
212
+ sent = []
213
+ received = []
214
+ violations = []
215
+ errors = []
216
+
217
+ for e in recent:
218
+ et = e.get('eventType', '')
219
+ msg = e.get('message', {}) or {}
220
+ ts = e.get('timestamp', '?')[:19]
221
+ outcome = e.get('outcome', '?')
222
+ fr = msg.get('from', '?')
223
+ to = msg.get('to', '?')
224
+ method = msg.get('method', '?')
225
+
226
+ if et == 'message_sent':
227
+ sent.append(f' {ts} {fr} → {to} [{method}] outcome={outcome}')
228
+ elif et == 'message_received':
229
+ received.append(f' {ts} {fr} → {to} [{method}] outcome={outcome}')
230
+ elif et == 'routing_violation':
231
+ violations.append(f' {ts} {fr} → {to} [{method}] reason={e.get(\"routingDecision\", \"?\")}'[:120])
232
+ elif outcome in ('error', 'failed'):
233
+ errors.append(f' {ts} {et} outcome={outcome} details={e.get(\"details\", \"?\")}'[:120])
234
+
235
+ verbose = '$VERBOSE' == 'true'
236
+ if sent:
237
+ print(f' 📤 Sent ({len(sent)}):')
238
+ for s in (sent if verbose else sent[-5:]):
239
+ print(s)
240
+ if not verbose and len(sent) > 5:
241
+ print(f' ... and {len(sent) - 5} more (use --verbose)')
242
+ if received:
243
+ print(f' 📥 Received ({len(received)}):')
244
+ for r in (received if verbose else received[-5:]):
245
+ print(r)
246
+ if not verbose and len(received) > 5:
247
+ print(f' ... and {len(received) - 5} more (use --verbose)')
248
+ if violations:
249
+ print(f' 🚫 Routing Violations ({len(violations)}):')
250
+ for v in violations:
251
+ print(v)
252
+ if errors:
253
+ print(f' 💥 Errors ({len(errors)}):')
254
+ for e in errors:
255
+ print(e)
256
+
257
+ if sent or received:
258
+ sys.exit(0)
259
+ elif violations or errors:
260
+ sys.exit(2)
261
+ else:
262
+ sys.exit(1)
263
+ " 2>/dev/null)
264
+
265
+ EXIT_CODE=$?
266
+
267
+ if [ $EXIT_CODE -eq 0 ]; then
268
+ HAS_MESSAGES=true
269
+ echo " ── $AGENT ──"
270
+ echo "$FLOW"
271
+ echo ""
272
+ elif [ $EXIT_CODE -eq 2 ]; then
273
+ HAS_MESSAGES=true
274
+ echo " ── $AGENT (issues detected) ──"
275
+ echo "$FLOW"
276
+ echo ""
277
+ fi
278
+ done
279
+
280
+ if ! $HAS_MESSAGES; then
281
+ echo " No fleet messages found in any agent's audit trail."
282
+ echo " This means agents haven't communicated via fleet-bus yet."
283
+ echo ""
284
+ echo " To trigger a test, ask Jessie to delegate a task to Sam:"
285
+ echo ' "Jessie, ask Sam to write a hello world script"'
286
+ echo ""
287
+ fi
288
+
289
+ ###############################################################################
290
+ # 5. Gateway Fleet-Bus Initialization
291
+ ###############################################################################
292
+ echo "=== 5. Gateway Initialization ==="
293
+
294
+ if command -v journalctl &>/dev/null; then
295
+ INIT_LOGS=$(journalctl -u openclaw-gateway --since "24 hours ago" --no-pager 2>/dev/null | grep -i 'fleet-bus.*initialized' || true)
296
+
297
+ if [ -n "$INIT_LOGS" ]; then
298
+ INIT_COUNT=$(echo "$INIT_LOGS" | wc -l | tr -d ' ')
299
+ check "Fleet-bus init logs" "PASS" "$INIT_COUNT initialization(s) in last 24h"
300
+
301
+ INITIALIZED_AGENTS=""
302
+ for AGENT in "${AGENTS[@]}"; do
303
+ if echo "$INIT_LOGS" | grep -q "fleet-bus:${AGENT}"; then
304
+ INITIALIZED_AGENTS="$INITIALIZED_AGENTS $AGENT"
305
+ fi
306
+ done
307
+
308
+ MISSING_AGENTS=""
309
+ for AGENT in "${AGENTS[@]}"; do
310
+ if ! echo "$INITIALIZED_AGENTS" | grep -q "$AGENT"; then
311
+ MISSING_AGENTS="$MISSING_AGENTS $AGENT"
312
+ fi
313
+ done
314
+
315
+ if [ -n "$MISSING_AGENTS" ]; then
316
+ check "Agent init coverage" "WARN" "not seen in logs:$MISSING_AGENTS"
317
+ else
318
+ check "Agent init coverage" "PASS" "all 6 agents initialized"
319
+ fi
320
+
321
+ if $VERBOSE; then
322
+ echo ""
323
+ echo " Recent init logs:"
324
+ echo "$INIT_LOGS" | tail -12 | while read -r line; do
325
+ echo " $line"
326
+ done
327
+ fi
328
+ else
329
+ check "Fleet-bus init logs" "WARN" "no fleet-bus initialization in last 24h"
330
+ fi
331
+ else
332
+ check "Fleet-bus init logs" "WARN" "journalctl not available — cannot check gateway logs"
333
+ fi
334
+ echo ""
335
+
336
+ ###############################################################################
337
+ # 6. Fleet-Bus Transport Test (requires --live)
338
+ ###############################################################################
339
+ if $LIVE_TEST; then
340
+ echo "=== 6. Live Transport Test (fleet/ping via callGateway) ==="
341
+
342
+ LIVE_SCRIPT=$(mktemp /tmp/fleet-live-ping-XXXXXX.mjs)
343
+ cat > "$LIVE_SCRIPT" << 'NODESCRIPT'
344
+ import { existsSync } from 'node:fs';
345
+ import { dirname, join } from 'node:path';
346
+ import { randomBytes } from 'node:crypto';
347
+
348
+ async function resolveCallGateway() {
349
+ const mainScript = process.argv[1] ?? '';
350
+ const home = process.env.HOME ?? '/root';
351
+ const packageNames = ['clawdbot', 'openclaw'];
352
+ const globalRoots = [
353
+ '/usr/lib/node_modules',
354
+ '/usr/local/lib/node_modules',
355
+ join(home, '.local', 'lib', 'node_modules'),
356
+ ];
357
+ const paths = globalRoots.flatMap(root =>
358
+ packageNames.map(pkg => join(root, pkg, 'dist', 'gateway', 'call.js'))
359
+ );
360
+ for (const p of paths) {
361
+ if (!existsSync(p)) continue;
362
+ try {
363
+ const mod = await import(`file://${p}`);
364
+ if (typeof mod.callGateway === 'function') return mod.callGateway;
365
+ } catch {}
366
+ }
367
+ return null;
368
+ }
369
+
370
+ try {
371
+ const callGateway = await resolveCallGateway();
372
+ if (!callGateway) {
373
+ console.log('RESULT:FAIL:callGateway not resolved — gateway/call.js not found');
374
+ process.exit(0);
375
+ }
376
+
377
+ const msg = {
378
+ id: `fm-diag-${Date.now()}-${randomBytes(4).toString('hex')}`,
379
+ from: 'mighty-mark',
380
+ to: 'jessie',
381
+ method: 'fleet/ping',
382
+ params: { source: 'diagnostics', timestamp: new Date().toISOString() },
383
+ timestamp: new Date().toISOString(),
384
+ nonce: randomBytes(8).toString('hex'),
385
+ version: '1.0',
386
+ };
387
+
388
+ const serialized = '%%FLEET%%' + JSON.stringify(msg);
389
+ const start = Date.now();
390
+
391
+ const result = await callGateway({
392
+ method: 'agent',
393
+ params: {
394
+ agentId: 'jessie',
395
+ message: serialized,
396
+ deliver: false,
397
+ timeoutSeconds: 15,
398
+ label: 'fleet:fleet/ping',
399
+ },
400
+ timeoutMs: 20000,
401
+ mode: 'backend',
402
+ clientName: 'fleet-bus-diagnostics',
403
+ clientDisplayName: 'Fleet Diagnostics',
404
+ });
405
+
406
+ const duration = Date.now() - start;
407
+ const hasResponse = result && (typeof result === 'object' || typeof result === 'string');
408
+
409
+ if (hasResponse) {
410
+ const summary = typeof result === 'string' ? result.slice(0, 100) : JSON.stringify(result).slice(0, 100);
411
+ console.log(`RESULT:PASS:fleet/ping delivered to jessie (${duration}ms) — response: ${summary}`);
412
+ } else {
413
+ console.log(`RESULT:PASS:fleet/ping accepted by gateway (${duration}ms) — fire-and-forget`);
414
+ }
415
+ } catch (err) {
416
+ console.log(`RESULT:FAIL:${err.message?.slice(0, 150) || 'unknown error'}`);
417
+ }
418
+ process.exit(0);
419
+ NODESCRIPT
420
+
421
+ LIVE_RESULT=$(node "$LIVE_SCRIPT" 2>/dev/null || echo "RESULT:FAIL:node execution failed")
422
+ rm -f "$LIVE_SCRIPT"
423
+
424
+ LIVE_STATUS=$(echo "$LIVE_RESULT" | grep '^RESULT:' | head -1 | cut -d: -f2)
425
+ LIVE_DETAIL=$(echo "$LIVE_RESULT" | grep '^RESULT:' | head -1 | cut -d: -f3-)
426
+
427
+ if [ "$LIVE_STATUS" = "PASS" ]; then
428
+ check "Live ping (Mark → Jessie)" "PASS" "$LIVE_DETAIL"
429
+ elif [ "$LIVE_STATUS" = "FAIL" ]; then
430
+ check "Live ping (Mark → Jessie)" "FAIL" "$LIVE_DETAIL"
431
+ else
432
+ check "Live ping (Mark → Jessie)" "WARN" "unexpected result: $LIVE_RESULT"
433
+ fi
434
+ echo ""
435
+ fi
436
+
437
+ ###############################################################################
438
+ # Summary
439
+ ###############################################################################
440
+ echo "══════════════════════════════════════════════"
441
+ echo " Results: $PASS passed, $WARN warnings, $FAIL failed"
442
+ echo "══════════════════════════════════════════════"
443
+ echo ""
444
+
445
+ if [ $FAIL -gt 0 ]; then
446
+ echo " ⚡ Fleet-bus has $FAIL failing checks — investigate above."
447
+ exit 1
448
+ elif [ $WARN -gt 0 ]; then
449
+ echo " ⚠️ $WARN warnings — review above for details."
450
+ exit 0
451
+ else
452
+ echo " 🎯 Fleet-bus is healthy."
453
+ exit 0
454
+ fi