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.
- package/INTEGRATION_GUIDE.md +1 -1
- package/QUICKSTART.md +70 -0
- package/README.md +14 -5
- package/SKILL.md +5 -5
- package/bin/cli.ts +277 -2
- package/dist/bin/cli.js +269 -1
- package/dist/bin/cli.js.map +1 -1
- package/dist/lib/auth-guardian.d.ts +18 -0
- package/dist/lib/auth-guardian.d.ts.map +1 -1
- package/dist/lib/auth-guardian.js +34 -0
- package/dist/lib/auth-guardian.js.map +1 -1
- package/dist/lib/locked-blackboard.d.ts +7 -0
- package/dist/lib/locked-blackboard.d.ts.map +1 -1
- package/dist/lib/locked-blackboard.js +11 -2
- package/dist/lib/locked-blackboard.js.map +1 -1
- package/package.json +1 -1
- package/scripts/swarm_guard.py +8 -3
package/INTEGRATION_GUIDE.md
CHANGED
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
|
[](https://network-ai.org/)
|
|
6
6
|
[](https://github.com/Jovancoding/Network-AI/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/Jovancoding/Network-AI/actions/workflows/codeql.yml)
|
|
8
|
-
[](https://github.com/Jovancoding/Network-AI/releases)
|
|
9
9
|
[](https://www.npmjs.com/package/network-ai)
|
|
10
10
|
[](#testing)
|
|
11
11
|
[](#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,
|
|
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
|
|
9
|
-
network_calls: "none —
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
.
|
|
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');
|