network-ai 5.7.2 → 5.8.1

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.
@@ -564,4 +564,4 @@ Run these before declaring the integration production-ready:
564
564
 
565
565
  ---
566
566
 
567
- *Network-AI v5.7.2 · MIT License · https://github.com/Jovancoding/Network-AI*
567
+ *Network-AI v5.8.1 · MIT License · https://github.com/Jovancoding/Network-AI*
package/QUICKSTART.md CHANGED
@@ -387,16 +387,86 @@ network-ai audit tail
387
387
  network-ai audit clear
388
388
  ```
389
389
 
390
+ ### Diagnostics (`doctor`)
391
+
392
+ ```bash
393
+ # Validate the full environment — data dir, env routing, audit log, WAL, kill-switch, MCP secret
394
+ network-ai doctor
395
+ network-ai doctor --json # machine-readable
396
+
397
+ # ✓ [PASS] data-dir: /path/to/data
398
+ # ✓ [PASS] env-routing: no --env or NETWORK_AI_ENV set; using root data dir
399
+ # ✓ [PASS] audit-log: 42 entries, all valid JSONL
400
+ # ✓ [PASS] pending-changes: no pending changes
401
+ # ✓ [PASS] kill-switch: system is running
402
+ # ⚠ [WARN] mcp-secret: NETWORK_AI_MCP_SECRET not set
403
+ # ✓ [PASS] blackboard-schema: blackboard.json OK (7 keys)
404
+ ```
405
+
406
+ Exits with code `0` if all checks pass, `1` if any fail — safe for CI gates.
407
+
408
+ ### Inspect a key (`inspect`)
409
+
410
+ ```bash
411
+ # Show current value and metadata
412
+ network-ai inspect agent:status
413
+
414
+ # Include pending WAL history
415
+ network-ai inspect agent:status --history
416
+
417
+ # Include audit trail entries for this key
418
+ network-ai inspect agent:status --audit
419
+
420
+ # All together, machine-readable
421
+ network-ai inspect agent:status --history --audit --json
422
+ ```
423
+
424
+ ### Kill switch (`pause` / `resume`)
425
+
426
+ ```bash
427
+ # Pause all orchestrator activity
428
+ network-ai pause
429
+ # ✓ system paused at 2026-05-23T15:00:00.000Z
430
+
431
+ # Resume
432
+ network-ai resume
433
+ # ✓ system resumed
434
+
435
+ # Check state
436
+ network-ai doctor # ⚠ [WARN] kill-switch: system is PAUSED
437
+ ```
438
+
439
+ Creates/removes a `data/SYSTEM_PAUSED` sentinel file. Agents should check for this file before performing writes.
440
+
441
+ ### `--why` on `auth token`
442
+
443
+ ```bash
444
+ # See the full scoring breakdown before the token is issued
445
+ network-ai auth token my-bot --resource DATABASE \
446
+ --justification "Fetch Q4 invoices for year-end report" --why
447
+
448
+ # justification score (40%): 80.0%
449
+ # trust score (30%): 100.0%
450
+ # risk score (30%): 50.0% risk → 50.0% contribution
451
+ # weighted score: 74.0%
452
+ # verdict: APPROVED
453
+ ```
454
+
390
455
  ### Global flags
391
456
 
392
457
  | Flag | Default | Purpose |
393
458
  |---|---|---|
394
459
  | `--data <path>` | `./data` | Override the data directory |
460
+ | `--env <name>` | — | Target environment (dev/st/sit/qa/preprod/prod) |
395
461
  | `--json` | off | Machine-readable JSON output on every command |
462
+ | `--minimal` | off | Skip WAL replay + TTL sweep (CI/test fast startup). Also set via `NETWORK_AI_MINIMAL=1` |
396
463
 
397
464
  ```bash
398
465
  # Example: point at a non-default data dir and get JSON output
399
466
  network-ai --data /var/swarm/data --json bb list
467
+
468
+ # CI mode — skip WAL replay for fast startup
469
+ NETWORK_AI_MINIMAL=1 network-ai doctor --json
400
470
  ```
401
471
 
402
472
  ---
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Website](https://img.shields.io/badge/website-network--ai.org-4b9df2?style=flat&logo=web&logoColor=white)](https://network-ai.org/)
6
6
  [![CI](https://github.com/Jovancoding/Network-AI/actions/workflows/ci.yml/badge.svg)](https://github.com/Jovancoding/Network-AI/actions/workflows/ci.yml)
7
7
  [![CodeQL](https://github.com/Jovancoding/Network-AI/actions/workflows/codeql.yml/badge.svg)](https://github.com/Jovancoding/Network-AI/actions/workflows/codeql.yml)
8
- [![Release](https://img.shields.io/badge/release-v5.7.2-blue.svg)](https://github.com/Jovancoding/Network-AI/releases)
8
+ [![Release](https://img.shields.io/badge/release-v5.8.1-blue.svg)](https://github.com/Jovancoding/Network-AI/releases)
9
9
  [![npm](https://img.shields.io/npm/dw/network-ai.svg?label=npm%20downloads)](https://www.npmjs.com/package/network-ai)
10
10
  [![Tests](https://img.shields.io/badge/tests-3136%20passing-brightgreen.svg)](#testing)
11
11
  [![Adapters](https://img.shields.io/badge/frameworks-29%20supported-blueviolet.svg)](#adapter-system)
@@ -126,6 +126,8 @@ Runs priority preemption, AuthGuardian permission gating, FSM governance, and co
126
126
  | ✅ Playground REPL | Interactive sandbox with mock agents for rapid prototyping |
127
127
  | ✅ Adapter test harness | Parameterized test battery for any adapter implementation |
128
128
  | ✅ IAuthValidator | Interface to decouple authorization from concrete AuthGuardian |
129
+ | ✅ Kill switch | `network-ai pause` / `resume` — `SYSTEM_PAUSED` sentinel; `doctor` self-diagnostics; `inspect <key>` metadata + audit trail |
130
+ | ✅ Minimal mode | `--minimal` / `NETWORK_AI_MINIMAL=1` — skips WAL replay and sweep for fast CI/test startup |
129
131
  | ✅ TypeScript native | ES2022 strict mode, zero native dependencies |
130
132
 
131
133
  ---
@@ -273,11 +275,15 @@ network-ai auth token my-bot --resource blackboard
273
275
  | Command group | What it controls |
274
276
  |---|---|
275
277
  | `network-ai bb` | Blackboard — get, set, delete, list, snapshot, propose, commit, abort |
276
- | `network-ai auth` | AuthGuardian — issue tokens, revoke, check permissions |
278
+ | `network-ai auth` | AuthGuardian — issue tokens (`--why` for scoring breakdown), revoke, check permissions |
277
279
  | `network-ai budget` | FederatedBudget — spend status, set ceiling |
278
280
  | `network-ai audit` | Audit log — print, live-tail, clear |
281
+ | `network-ai env` | Environment management — init, list, chain, diff, promote, backup, restore |
282
+ | `network-ai doctor` | Self-diagnostics — validate data dir, env routing, audit log, WAL, kill-switch, MCP secret |
283
+ | `network-ai inspect <key>` | Inspect a blackboard key — value, metadata, pending history, audit trail |
284
+ | `network-ai pause` / `resume` | Kill switch — write/remove `SYSTEM_PAUSED` sentinel |
279
285
 
280
- Global flags on every command: `--data <path>` (data directory, default `./data`) · `--json` (machine-readable output)
286
+ Global flags on every command: `--data <path>` (data directory, default `./data`) · `--env <name>` (environment) · `--json` (machine-readable output) · `--minimal` (skip WAL replay + sweep — CI/test fast startup)
281
287
 
282
288
  → Full reference in [QUICKSTART.md § CLI](QUICKSTART.md)
283
289
 
@@ -478,14 +484,17 @@ npm run test:phase12 # Context Throttler, Partition Planner, Coverage Gate,
478
484
  | [QUICKSTART.md](QUICKSTART.md) | Installation, first run, CLI reference, PowerShell guide, Python scripts CLI |
479
485
  | [ARCHITECTURE.md](ARCHITECTURE.md) | Race condition problem, FSM design, handoff protocol, module inventory, project structure |
480
486
  | [BENCHMARKS.md](BENCHMARKS.md) | Provider performance, rate limits, local GPU, `max_completion_tokens` guide |
481
- | [SECURITY.md](SECURITY.md) | Security module, permission system, trust levels, audit trail, v5.0 security additions, ClawHub scan findings |
487
+ | [SECURITY.md](SECURITY.md) | Security module, permission system, trust levels, audit trail, disclosure SLA, ClawHub scan findings |
488
+ | [THREAT_MODEL.md](THREAT_MODEL.md) | Adversary profiles, trust boundaries, explicit non-goals, security controls summary |
489
+ | [DATA_LOCATIONS.md](DATA_LOCATIONS.md) | Every file Network-AI creates — path, purpose, data classification, operator responsibilities |
490
+ | [SUPPLY_CHAIN.md](SUPPLY_CHAIN.md) | Runtime dependencies, what runs at install, network surface, SLSA/npm provenance verification |
482
491
  | [ENTERPRISE.md](ENTERPRISE.md) | Evaluation checklist, stability policy, security summary, integration entry points |
483
492
  | [AUDIT_LOG_SCHEMA.md](AUDIT_LOG_SCHEMA.md) | Audit log field reference, all event types, scoring formula |
484
493
  | [ADOPTERS.md](ADOPTERS.md) | Known adopters — open a PR to add yourself |
485
494
  | [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) | End-to-end integration walkthrough with v5.0 modules |
486
495
  | [SKILL.md](SKILL.md) | OpenClaw/ClawHub Python skill — setup, orchestrator protocol, security scan findings |
487
496
  | [references/adapter-system.md](references/adapter-system.md) | Adapter architecture, all 29 adapters, writing custom adapters |
488
- | [references/auth-guardian.md](references/auth-guardian.md) | Permission scoring, resource types, IAuthValidator interface |
497
+ | [references/auth-guardian.md](references/auth-guardian.md) | Permission scoring, resource types, `scoreRequest()`, IAuthValidator interface |
489
498
  | [references/trust-levels.md](references/trust-levels.md) | Trust level configuration, APS delegation-chain mapping |
490
499
 
491
500
  ---
package/SKILL.md CHANGED
@@ -5,8 +5,8 @@ metadata:
5
5
  openclaw:
6
6
  emoji: "\U0001F41D"
7
7
  homepage: https://network-ai.org
8
- bundle_scope: "Python scripts only (scripts/*.py). All execution is local. Only Python stdlib no other runtimes, adapters, or CLI tools are included."
9
- network_calls: "none — bundled scripts make zero network calls and spawn no subprocesses."
8
+ bundle_scope: "Python scripts (scripts/*.py) local only, Python stdlib only, no network calls, no subprocesses. The full npm package additionally includes TypeScript library modules, a CLI (bin/cli.ts), and an optional self-hosted MCP SSE server (bin/mcp-server.ts) that binds a TCP port when started by the operator. Install the npm package only if you intend to run the full orchestrator."
9
+ network_calls: "bundled Python scripts: none — zero network calls, zero subprocesses. MCP SSE server (bin/mcp-server.ts, optional): binds a TCP port (default 127.0.0.1) when explicitly started by the operator; requires a non-empty secret (bearer token). Core TypeScript library: zero outbound network calls all LLM/API clients are BYOC (bring your own client)."
10
10
  inter_agent_comms: "none — this skill does not implement, invoke, or control inter-agent messaging or sessions_send. All coordination is via local file-based blackboard only."
11
11
  sessions_send: "NOT implemented or invoked by this skill. sessions_send is a host-platform built-in entirely outside this skill's control. See data-flow notice below."
12
12
  sessions_ops: "platform-provided — outside this skill's control"
@@ -713,7 +713,7 @@ The following findings are drawn from the **MAESTRO Agent Security Threat** fram
713
713
 
714
714
  | Control | How Network-AI addresses it |
715
715
  |---|---|
716
- | **Permission manifest** | `metadata.openclaw` in SKILL.md frontmatter explicitly declares `bundle_scope: "Python scripts only"`, `network_calls: none`, `requires.bins: [python3]` — no shell tools, no API credentials, no external services |
716
+ | **Permission manifest** | `metadata.openclaw` in SKILL.md frontmatter explicitly declares `bundle_scope` (Python scripts: local-only; full npm package: includes optional MCP SSE server), `network_calls` (Python scripts: none; MCP SSE server: TCP, operator-started, bearer-token required), `requires.bins: [python3]` — no API credentials, no external services in core |
717
717
  | **Least-privilege resource gating** | `check_permission.py` uses a weighted scoring model (justification 40 %, trust 30 %, risk 30 %); PAYMENTS and FILE_EXPORT require `--confirm-high-risk` acknowledgment before any token is issued; `--scope` limits every grant to minimum required access |
718
718
  | **Abstract resource labels only** | PAYMENTS, DATABASE, EMAIL, FILE_EXPORT are local scoring labels — no external credentials exist in the skill; there is nothing to leak to an external service |
719
719
  | **HMAC-signed grant tokens** | Since v5.5.2, every grant record carries `_sig` (HMAC-SHA256 over canonical fields); `validate_token.py` rejects tampered records — privilege escalation via forged grants is detected at validation time |
@@ -726,7 +726,7 @@ The following findings are drawn from the **MAESTRO Agent Security Threat** fram
726
726
 
727
727
  | Control | How Network-AI addresses it |
728
728
  |---|---|
729
- | **Zero network calls, zero subprocesses** | All bundled Python scripts use Python stdlib only, spawn no subprocesses, and make no network calls — declared in `metadata.openclaw.network_calls: none` and `bundle_scope`; enforceable by platform inspection |
729
+ | **Zero network calls (Python scripts)** | All bundled Python scripts use Python stdlib only, spawn no subprocesses, and make no network calls — declared in `metadata.openclaw.network_calls` and `bundle_scope`. The optional MCP SSE server (`bin/mcp-server.ts`) binds a TCP port only when explicitly started by the operator and requires a non-empty bearer-token secret. |
730
730
  | **AgentRuntime sandbox** | `ShellExecutor` enforces per-command timeout and output-size limits; `SandboxPolicy` allowlist/blocklist prevents unapproved shell commands from running at all |
731
731
  | **Source protection** | `SandboxPolicy.sourceProtection` constrains `FileAccessor.read/write/list` to `data/<env>/` only; any attempt to read outside that boundary throws `SourceProtectionError` — the agent receives `{success: false}`, no path details leak |
732
732
  | **Environment isolation** | `NETWORK_AI_ENV` / `--env` routes all state to `data/<env>/`; dev, staging, and production state are fully separated; live state (`audit_log.jsonl`, `active_grants.json`) never promotes across environments |
@@ -739,7 +739,7 @@ The following findings are drawn from the **MAESTRO Agent Security Threat** fram
739
739
 
740
740
  | Control | How Network-AI addresses it |
741
741
  |---|---|
742
- | **Exact version pinning** | npm `package.json` uses exact `"version": "5.7.2"` — no semver range specifiers; `clawhub install network-ai` pins to a specific published version |
742
+ | **Exact version pinning** | npm `package.json` uses exact `"version": "5.8.1"` — no semver range specifiers; `clawhub install network-ai` pins to a specific published version |
743
743
  | **Zero transitive dependency drift** | All bundled Python scripts use Python stdlib only — `pip install` is never required; there are no third-party packages to drift, be compromised upstream, or introduce CVEs |
744
744
  | **Signed, tagged releases** | Every release is committed with a signed Git tag (`v5.7.x`); commit hash is verifiable against CHANGELOG.md; GitHub releases link tag → diff → changelog entry |
745
745
  | **Supply chain monitoring** | npm package continuously scored by Socket.dev (score A); any new dependency or permission change triggers an alert |
package/bin/cli.ts CHANGED
@@ -65,7 +65,8 @@ program
65
65
  .enablePositionalOptions()
66
66
  .addOption(new Option('--data <path>', 'path to data directory').default('./data'))
67
67
  .addOption(new Option('--env <name>', 'target environment (dev|st|sit|qa|sandbox|preprod|prod)'))
68
- .addOption(new Option('--json', 'output raw JSON (useful for piping)'));
68
+ .addOption(new Option('--json', 'output raw JSON (useful for piping)'))
69
+ .addOption(new Option('--minimal', 'disable WAL, TTL sweep, and telemetry hooks (CI/test mode); also set via NETWORK_AI_MINIMAL=1'));
69
70
 
70
71
  // ── bb (blackboard) ───────────────────────────────────────────────────────────
71
72
 
@@ -165,11 +166,27 @@ auth.command('token <agentId>')
165
166
  .option('--resource <type>', 'resource type to grant', 'blackboard')
166
167
  .option('--justification <text>', 'justification text', 'CLI-issued token')
167
168
  .option('--scope <scope>', 'permission scope')
168
- .action(async (agentId: string, opts: { resource: string; justification: string; scope?: string }, cmd: Command) => {
169
+ .option('--why', 'show scoring breakdown (justification/trust/risk) before issuing')
170
+ .action(async (agentId: string, opts: { resource: string; justification: string; scope?: string; why?: boolean }, cmd: Command) => {
169
171
  const g = cmd.optsWithGlobals<{ data: string; json: boolean }>();
170
172
  const auditPath = path.join(resolveData(g), 'audit_log.jsonl');
171
173
  const guardian = new AuthGuardian({ auditLogPath: auditPath });
172
174
  guardian.registerAgentTrust({ agentId, trustLevel: 1, allowedResources: [opts.resource] });
175
+
176
+ if (opts.why) {
177
+ const scoring = guardian.scoreRequest(agentId, opts.resource, opts.justification, opts.scope);
178
+ if (g.json) {
179
+ print(scoring, true);
180
+ } else {
181
+ console.log(`justification score (40%): ${(scoring.justificationScore * 100).toFixed(1)}%`);
182
+ console.log(`trust score (30%): ${(scoring.trustScore * 100).toFixed(1)}%`);
183
+ console.log(`risk score (30%): ${(scoring.riskScore * 100).toFixed(1)}% risk → ${((1 - scoring.riskScore) * 100).toFixed(1)}% contribution`);
184
+ console.log(`weighted score: ${(scoring.weightedScore * 100).toFixed(1)}%`);
185
+ console.log(`verdict: ${scoring.approved ? 'APPROVED' : 'DENIED'}${scoring.reason ? ` — ${scoring.reason}` : ''}`);
186
+ console.log('');
187
+ }
188
+ }
189
+
173
190
  const grant = await guardian.requestPermission(agentId, opts.resource, opts.justification, opts.scope);
174
191
  if (g.json) {
175
192
  print(grant, true);
@@ -455,12 +472,270 @@ envBackup.command('prune')
455
472
  print(g.json ? { deleted } : `✓ pruned ${deleted} backup(s), keeping ${opts.keep}`, g.json);
456
473
  });
457
474
 
475
+ // ── doctor ────────────────────────────────────────────────────────────────────
476
+
477
+ program.command('doctor')
478
+ .description('Validate the Network-AI environment and configuration')
479
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
480
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
481
+ const dataDir = resolveData(g);
482
+
483
+ const results: Array<{ check: string; status: 'pass' | 'warn' | 'fail'; detail: string }> = [];
484
+ let exitCode = 0;
485
+
486
+ function check(name: string, fn: () => { status: 'pass' | 'warn' | 'fail'; detail: string }): void {
487
+ try {
488
+ results.push({ check: name, ...fn() });
489
+ } catch (err) {
490
+ results.push({ check: name, status: 'fail', detail: err instanceof Error ? err.message : String(err) });
491
+ }
492
+ }
493
+
494
+ // 1 — Data directory exists and is writable
495
+ check('data-dir', () => {
496
+ if (!fs.existsSync(dataDir)) {
497
+ return { status: 'warn', detail: `data dir does not exist: ${dataDir} (will be created on first write)` };
498
+ }
499
+ try {
500
+ fs.accessSync(dataDir, fs.constants.W_OK);
501
+ return { status: 'pass', detail: dataDir };
502
+ } catch {
503
+ return { status: 'fail', detail: `data dir is not writable: ${dataDir}` };
504
+ }
505
+ });
506
+
507
+ // 2 — NETWORK_AI_ENV routing
508
+ check('env-routing', () => {
509
+ const envVar = process.env['NETWORK_AI_ENV'];
510
+ if (g.env) {
511
+ return { status: 'pass', detail: `--env ${g.env} (CLI flag)` };
512
+ }
513
+ if (envVar) {
514
+ return { status: 'pass', detail: `NETWORK_AI_ENV=${envVar}` };
515
+ }
516
+ return { status: 'warn', detail: 'no --env or NETWORK_AI_ENV set; using root data dir' };
517
+ });
518
+
519
+ // 3 — Audit log integrity (valid JSONL)
520
+ check('audit-log', () => {
521
+ const logFile = getAuditLogPath(dataDir);
522
+ if (!fs.existsSync(logFile)) {
523
+ return { status: 'warn', detail: 'audit log does not exist yet' };
524
+ }
525
+ const lines = fs.readFileSync(logFile, 'utf8').split('\n').filter(l => l.trim());
526
+ let badLines = 0;
527
+ for (const line of lines) {
528
+ try { JSON.parse(line); } catch { badLines++; }
529
+ }
530
+ if (badLines > 0) {
531
+ return { status: 'fail', detail: `${badLines} of ${lines.length} lines are not valid JSON` };
532
+ }
533
+ return { status: 'pass', detail: `${lines.length} entries, all valid JSONL` };
534
+ });
535
+
536
+ // 4 — Pending changes (stale WAL entries)
537
+ check('pending-changes', () => {
538
+ const pendingDir = path.join(dataDir, 'pending_changes');
539
+ if (!fs.existsSync(pendingDir)) {
540
+ return { status: 'pass', detail: 'no pending_changes dir' };
541
+ }
542
+ const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.json'));
543
+ if (files.length === 0) {
544
+ return { status: 'pass', detail: 'no pending changes' };
545
+ }
546
+ // Flag as warn if any are older than 5 minutes
547
+ const stale = files.filter(f => {
548
+ try {
549
+ const st = fs.statSync(path.join(pendingDir, f));
550
+ return (Date.now() - st.mtimeMs) > 5 * 60 * 1000;
551
+ } catch { return false; }
552
+ });
553
+ if (stale.length > 0) {
554
+ return { status: 'warn', detail: `${stale.length} stale pending change(s) (>5 min old)` };
555
+ }
556
+ return { status: 'pass', detail: `${files.length} in-flight pending change(s)` };
557
+ });
558
+
559
+ // 5 — System paused?
560
+ check('kill-switch', () => {
561
+ const sentinel = path.join(dataDir, 'SYSTEM_PAUSED');
562
+ if (fs.existsSync(sentinel)) {
563
+ return { status: 'warn', detail: 'system is PAUSED (run "network-ai resume" to unpause)' };
564
+ }
565
+ return { status: 'pass', detail: 'system is running' };
566
+ });
567
+
568
+ // 6 — MCP secret configured (env var)
569
+ check('mcp-secret', () => {
570
+ const secret = process.env['NETWORK_AI_MCP_SECRET'];
571
+ if (!secret) {
572
+ return { status: 'warn', detail: 'NETWORK_AI_MCP_SECRET not set; McpSseServer will refuse to start without a secret' };
573
+ }
574
+ return { status: 'pass', detail: 'NETWORK_AI_MCP_SECRET is set' };
575
+ });
576
+
577
+ // 7 — Blackboard file is valid JSON (if it exists)
578
+ check('blackboard-schema', () => {
579
+ const bbFile = path.join(dataDir, 'blackboard.json');
580
+ if (!fs.existsSync(bbFile)) {
581
+ return { status: 'pass', detail: 'blackboard file does not exist yet' };
582
+ }
583
+ try {
584
+ const raw = fs.readFileSync(bbFile, 'utf8');
585
+ const parsed = JSON.parse(raw) as unknown;
586
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
587
+ return { status: 'fail', detail: 'blackboard.json is not a JSON object' };
588
+ }
589
+ return { status: 'pass', detail: `blackboard.json OK (${Object.keys(parsed as Record<string, unknown>).length} keys)` };
590
+ } catch (e) {
591
+ return { status: 'fail', detail: `blackboard.json parse error: ${e instanceof Error ? e.message : String(e)}` };
592
+ }
593
+ });
594
+
595
+ // Determine exit code
596
+ for (const r of results) {
597
+ if (r.status === 'fail') exitCode = 1;
598
+ }
599
+
600
+ if (g.json) {
601
+ print({ checks: results, ok: exitCode === 0 }, true);
602
+ } else {
603
+ for (const r of results) {
604
+ const icon = r.status === 'pass' ? '✓' : r.status === 'warn' ? '⚠' : '✗';
605
+ console.log(`${icon} [${r.status.toUpperCase().padEnd(4)}] ${r.check}: ${r.detail}`);
606
+ }
607
+ if (exitCode === 0) {
608
+ console.log('\nAll checks passed.');
609
+ } else {
610
+ console.log('\nOne or more checks failed.');
611
+ }
612
+ }
613
+
614
+ process.exit(exitCode);
615
+ });
616
+
617
+ // ── inspect ───────────────────────────────────────────────────────────────────
618
+
619
+ program.command('inspect <key>')
620
+ .description('Inspect a blackboard key: value, metadata, audit trail')
621
+ .option('--history', 'show WAL/pending version history')
622
+ .option('--audit', 'show audit log entries for this key')
623
+ .action((key: string, opts: { history?: boolean; audit?: boolean }, cmd: Command) => {
624
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
625
+ const dataDir = resolveData(g);
626
+
627
+ const bb = new LockedBlackboard(dataDir);
628
+ const entry = bb.read(key);
629
+
630
+ const result: Record<string, unknown> = {
631
+ key,
632
+ exists: entry !== null,
633
+ value: entry?.value ?? null,
634
+ metadata: entry ? {
635
+ source_agent: entry.source_agent,
636
+ timestamp: entry.timestamp,
637
+ ttl: entry.ttl,
638
+ version: entry.version,
639
+ } : null,
640
+ };
641
+
642
+ if (opts.history) {
643
+ const pendingDir = path.join(dataDir, 'pending_changes');
644
+ const history: unknown[] = [];
645
+ if (fs.existsSync(pendingDir)) {
646
+ const files = fs.readdirSync(pendingDir)
647
+ .filter(f => f.endsWith('.json'))
648
+ .sort();
649
+ for (const f of files) {
650
+ try {
651
+ const raw = JSON.parse(fs.readFileSync(path.join(pendingDir, f), 'utf8')) as Record<string, unknown>;
652
+ if (raw['key'] === key) history.push(raw);
653
+ } catch { /* skip malformed */ }
654
+ }
655
+ }
656
+ result['pendingHistory'] = history;
657
+ }
658
+
659
+ if (opts.audit) {
660
+ const logFile = getAuditLogPath(dataDir);
661
+ const auditEntries: unknown[] = [];
662
+ if (fs.existsSync(logFile)) {
663
+ const lines = fs.readFileSync(logFile, 'utf8').split('\n').filter(l => l.trim());
664
+ for (const line of lines) {
665
+ try {
666
+ const entry2 = JSON.parse(line) as Record<string, unknown>;
667
+ if (entry2['key'] === key) auditEntries.push(entry2);
668
+ } catch { /* skip */ }
669
+ }
670
+ }
671
+ result['auditTrail'] = auditEntries;
672
+ }
673
+
674
+ if (g.json) {
675
+ print(result, true);
676
+ } else {
677
+ console.log(`key: ${key}`);
678
+ console.log(`exists: ${result['exists']}`);
679
+ if (result['exists']) {
680
+ console.log(`value: ${JSON.stringify(result['value'], null, 2)}`);
681
+ if (result['metadata']) {
682
+ console.log(`meta: ${JSON.stringify(result['metadata'], null, 2)}`);
683
+ }
684
+ }
685
+ if (opts.history && Array.isArray(result['pendingHistory'])) {
686
+ const h = result['pendingHistory'] as unknown[];
687
+ console.log(`\npending history (${h.length} entries):`);
688
+ h.forEach((e, i) => console.log(` [${i + 1}] ${JSON.stringify(e)}`));
689
+ }
690
+ if (opts.audit && Array.isArray(result['auditTrail'])) {
691
+ const a = result['auditTrail'] as unknown[];
692
+ console.log(`\naudit trail (${a.length} entries):`);
693
+ a.forEach((e, i) => console.log(` [${i + 1}] ${JSON.stringify(e)}`));
694
+ }
695
+ }
696
+ });
697
+
698
+ // ── pause / resume (kill switch) ──────────────────────────────────────────────
699
+
700
+ program.command('pause')
701
+ .description('Pause all orchestrator activity (writes SYSTEM_PAUSED sentinel)')
702
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
703
+ const g = cmd.optsWithGlobals<{ data: string; json: boolean }>();
704
+ const dataDir = resolveData(g);
705
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
706
+ const sentinel = path.join(dataDir, 'SYSTEM_PAUSED');
707
+ const ts = new Date().toISOString();
708
+ fs.writeFileSync(sentinel, `paused at ${ts}\n`, 'utf8');
709
+ print(g.json ? { paused: true, at: ts, sentinel } : `✓ system paused at ${ts}`, g.json);
710
+ });
711
+
712
+ program.command('resume')
713
+ .description('Resume orchestrator activity (removes SYSTEM_PAUSED sentinel)')
714
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
715
+ const g = cmd.optsWithGlobals<{ data: string; json: boolean }>();
716
+ const dataDir = resolveData(g);
717
+ const sentinel = path.join(dataDir, 'SYSTEM_PAUSED');
718
+ if (!fs.existsSync(sentinel)) {
719
+ print(g.json ? { paused: false, detail: 'system was not paused' } : '✓ system is not paused', g.json);
720
+ return;
721
+ }
722
+ fs.unlinkSync(sentinel);
723
+ print(g.json ? { paused: false, resumed: true } : '✓ system resumed', g.json);
724
+ });
725
+
458
726
  // ── parse ─────────────────────────────────────────────────────────────────────
459
727
 
460
728
  // Auto-detect MCP stdio mode: when stdin is piped (not a TTY) and no
461
729
  // subcommand was given, start the MCP server in stdio transport mode.
462
730
  // This is the convention used by Glama, Claude Desktop, Cursor, etc.
463
731
  const userArgs = process.argv.slice(2);
732
+
733
+ // Propagate --minimal flag to env var before any commands run so that
734
+ // LockedBlackboard and other components can check it in their constructors.
735
+ if (userArgs.includes('--minimal') || process.env['NETWORK_AI_MINIMAL'] === '1') {
736
+ process.env['NETWORK_AI_MINIMAL'] = '1';
737
+ }
738
+
464
739
  if (!process.stdin.isTTY && userArgs.length === 0) {
465
740
  // Set --stdio before importing so the server module picks it up
466
741
  process.argv.push('--stdio');