@cirvix_ai/agent-control 0.1.3 → 0.2.0
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/README.md +76 -17
- package/bin/cirvix.mjs +539 -85
- package/bin/escape-benchmark.mjs +67 -0
- package/package.json +36 -16
- package/src/adapters/base.mjs +150 -0
- package/src/adapters/claude-code.mjs +161 -0
- package/src/adapters/cline.mjs +107 -0
- package/src/adapters/codex.mjs +104 -0
- package/src/adapters/cursor.mjs +104 -0
- package/src/adapters/frameworks.mjs +110 -0
- package/src/adapters/gemini-cli.mjs +104 -0
- package/src/adapters/generic-mcp.mjs +101 -0
- package/src/adapters/index.mjs +209 -0
- package/src/adapters/roo-code.mjs +106 -0
- package/src/adapters/vscode.mjs +104 -0
- package/src/adapters/windsurf.mjs +107 -0
- package/src/commands/console.mjs +58 -0
- package/src/commands/demo.mjs +55 -124
- package/src/commands/doctor.mjs +235 -0
- package/src/commands/init.mjs +292 -30
- package/src/commands/interactive.mjs +690 -0
- package/src/commands/kill.mjs +74 -0
- package/src/commands/login.mjs +227 -0
- package/src/commands/onboard.mjs +52 -0
- package/src/commands/passport.mjs +149 -0
- package/src/commands/policy.mjs +10 -6
- package/src/commands/protect.mjs +293 -0
- package/src/commands/prove.mjs +209 -0
- package/src/commands/redteam.mjs +51 -0
- package/src/commands/scan.mjs +11 -9
- package/src/commands/shadow.mjs +62 -0
- package/src/commands/simulate.mjs +96 -0
- package/src/commands/status.mjs +122 -41
- package/src/commands/upgrade.mjs +11 -11
- package/src/commands/welcome.mjs +105 -0
- package/src/core/authority.mjs +909 -0
- package/src/core/baseline.mjs +97 -0
- package/src/core/config-store.mjs +280 -0
- package/src/core/cost.mjs +0 -0
- package/src/core/detect.mjs +4 -33
- package/src/core/entitlements.mjs +7 -24
- package/src/core/escape-benchmark.mjs +597 -0
- package/src/core/events.mjs +234 -0
- package/src/core/evidence.mjs +212 -0
- package/src/core/format.mjs +44 -18
- package/src/core/gateway.mjs +15 -211
- package/src/core/graph.mjs +270 -0
- package/src/core/guard.mjs +118 -4
- package/src/core/intent.mjs +166 -0
- package/src/core/journal.mjs +131 -40
- package/src/core/kill-switch.mjs +122 -0
- package/src/core/notices.mjs +22 -2
- package/src/core/packs.mjs +193 -0
- package/src/core/passport.mjs +555 -0
- package/src/core/pipeline.mjs +148 -6
- package/src/core/prompts.mjs +51 -0
- package/src/core/proof.mjs +440 -0
- package/src/core/redteam/index.mjs +185 -0
- package/src/core/referral.mjs +187 -0
- package/src/core/sandbox.mjs +139 -0
- package/src/core/session.mjs +172 -0
- package/src/core/shadow.mjs +95 -0
- package/src/core/theme.mjs +240 -0
- package/src/core/trifecta.mjs +321 -0
- package/src/core/ui/controller.mjs +192 -0
- package/src/core/ui/decisions.mjs +55 -0
- package/src/core/ui/index.mjs +49 -0
- package/src/core/ui/intercept.mjs +103 -0
- package/src/core/ui/live.mjs +51 -0
- package/src/core/ui/primitives.mjs +123 -0
- package/src/core/ui/theme.mjs +92 -0
- package/src/core/verified.mjs +108 -0
- package/src/core/windows.mjs +270 -0
- package/src/index.mjs +67 -0
- package/src/tui/activity.mjs +71 -0
- package/src/tui/app.mjs +292 -0
- package/src/tui/cards.mjs +235 -0
- package/src/tui/composer.mjs +88 -0
- package/src/tui/palette.mjs +48 -0
- package/src/tui/status.mjs +42 -0
- package/src/core/cinematic.mjs +0 -545
package/bin/cirvix.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { access, mkdir, readFile } from "node:fs/promises";
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
14
|
|
|
15
15
|
import { evaluate, parseRules, STARTER_RULES } from "../src/core/policy.mjs";
|
|
16
16
|
import { AuditChain } from "../src/core/audit.mjs";
|
|
@@ -18,7 +18,11 @@ import { Daemon } from "../src/core/daemon.mjs";
|
|
|
18
18
|
import { Gateway } from "../src/core/gateway.mjs";
|
|
19
19
|
import { MessageFramer, serialize } from "../src/core/jsonrpc.mjs";
|
|
20
20
|
import { scan } from "../src/commands/scan.mjs";
|
|
21
|
-
import { bold, dim, green, red, amber, blue, plural } from "../src/core/format.mjs";
|
|
21
|
+
import { bold, dim, green, red, amber, blue, cyan, gray, plural } from "../src/core/format.mjs";
|
|
22
|
+
import { SHARE_URL } from "../src/core/prompts.mjs";
|
|
23
|
+
import { shouldAnimate } from "../src/core/ui/controller.mjs";
|
|
24
|
+
import { brandHeader, panel } from "../src/core/ui/primitives.mjs";
|
|
25
|
+
import { LiveStream } from "../src/core/ui/live.mjs";
|
|
22
26
|
|
|
23
27
|
import { MODE, DECISION } from "../src/core/decisions.mjs";
|
|
24
28
|
import { Pipeline } from "../src/core/pipeline.mjs";
|
|
@@ -27,13 +31,18 @@ import { ApprovalStore } from "../src/core/approvals.mjs";
|
|
|
27
31
|
import { UdsServer, defaultEndpoint, writeToken } from "../src/core/uds.mjs";
|
|
28
32
|
import * as journal from "../src/core/journal.mjs";
|
|
29
33
|
import * as policyCmd from "../src/commands/policy.mjs";
|
|
34
|
+
import * as protectCmd from "../src/commands/protect.mjs";
|
|
35
|
+
import * as proveCmd from "../src/commands/prove.mjs";
|
|
36
|
+
import * as passportCmd from "../src/commands/passport.mjs";
|
|
30
37
|
import { init as initCmd } from "../src/commands/init.mjs";
|
|
31
38
|
import { status as statusCmd } from "../src/commands/status.mjs";
|
|
32
39
|
import { upgrade as upgradeCmd } from "../src/commands/upgrade.mjs";
|
|
33
40
|
import { AgentRegistry, Meter, readLicence } from "../src/core/meter.mjs";
|
|
34
41
|
import { commercialNotices } from "../src/core/notices.mjs";
|
|
35
42
|
import { demo as demoCmd } from "../src/commands/demo.mjs";
|
|
36
|
-
import {
|
|
43
|
+
import { welcome } from "../src/commands/welcome.mjs";
|
|
44
|
+
import { doctor } from "../src/commands/doctor.mjs";
|
|
45
|
+
import { login, logout } from "../src/commands/login.mjs";
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* Read from the manifest, never written down twice.
|
|
@@ -57,11 +66,23 @@ const HELP = `
|
|
|
57
66
|
cirvix <command> [options]
|
|
58
67
|
|
|
59
68
|
${bold("GETTING STARTED")}
|
|
69
|
+
console Interactive runtime authorization (the product UI)
|
|
70
|
+
onboard 10-second guided first run
|
|
60
71
|
init Detect agents and MCP servers, write a policy, start protecting
|
|
72
|
+
init --apply Safely wire detected agents with pre-integration backup
|
|
73
|
+
init --dry-run Preview agent configuration changes without modifying files
|
|
74
|
+
init --rollback [id] Revert agent configurations to pre-integration state
|
|
61
75
|
status Runtime, policy, servers, blocked, approvals, P99 overhead
|
|
76
|
+
doctor diagnose this installation: policy, state, daemon, control plane
|
|
77
|
+
login / logout link this machine to your CIRVIX control plane (browser or --key)
|
|
62
78
|
upgrade Today's usage against your plan, and what lifts the limit
|
|
63
79
|
demo Watch an injected exfiltration attempt get stopped, live
|
|
80
|
+
protect [path] Discover, analyse, apply policy and prove it decides
|
|
81
|
+
passport [agent] What an agent is, by what it has actually done
|
|
82
|
+
prove <decision-id> Sign a decision into a portable proof artifact
|
|
83
|
+
verify <proof> Check a proof offline: signature, chain, integrity
|
|
64
84
|
scan Inventory what is ungoverned on this machine
|
|
85
|
+
theme Change appearance (dark|light|midnight|high-contrast|monochrome)
|
|
65
86
|
|
|
66
87
|
${bold("ENFORCEMENT")}
|
|
67
88
|
gateway Run the MCP gateway — intercepts and enforces
|
|
@@ -285,31 +306,106 @@ async function main() {
|
|
|
285
306
|
return 0;
|
|
286
307
|
}
|
|
287
308
|
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
// BARE `cirvix`:
|
|
310
|
+
// first run in a workspace -> onboarding (what CIRVIX is, three commands)
|
|
311
|
+
// returning, interactive -> the full live terminal
|
|
312
|
+
// returning, piped/CI -> the measured digest + next steps (no animation)
|
|
313
|
+
if (positional.length === 0 && !flags.json && !flags.help) {
|
|
314
|
+
let firstRun = false;
|
|
315
|
+
try { await access(join(cwd, ".cirvix")); } catch { firstRun = true; }
|
|
316
|
+
if (!firstRun) {
|
|
317
|
+
const { canLaunchInteractive } = await import("../src/commands/interactive.mjs");
|
|
318
|
+
if (canLaunchInteractive(flags, positional)) {
|
|
319
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
320
|
+
const { interactive } = await import("../src/commands/interactive.mjs");
|
|
321
|
+
await interactive({ cwd, flags, rules });
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
await welcome({ cwd });
|
|
326
|
+
return 0;
|
|
303
327
|
}
|
|
304
328
|
|
|
305
329
|
switch (command) {
|
|
330
|
+
case "protect": {
|
|
331
|
+
/* Shares policy resolution with `runtime` and `gateway`. A protect that
|
|
332
|
+
read policy differently from the runtime would be proving a decision
|
|
333
|
+
the runtime will not make. */
|
|
334
|
+
const target = sub && !sub.startsWith("-") ? resolve(cwd, sub) : cwd;
|
|
335
|
+
const rules = await loadRules(flags.policy, target);
|
|
336
|
+
const { result, output } = await protectCmd.protect({
|
|
337
|
+
cwd: target,
|
|
338
|
+
rules,
|
|
339
|
+
agent: String(flags.agent ?? "local"),
|
|
340
|
+
environment: String(flags.env ?? "local"),
|
|
341
|
+
json: Boolean(flags.json),
|
|
342
|
+
pace: flags.fast ? 0 : Number(flags.pace ?? 90),
|
|
343
|
+
animate: flags["no-animation"] ? false : undefined,
|
|
344
|
+
stateDir: stateDirFor(flags, target),
|
|
345
|
+
});
|
|
346
|
+
if (output) process.stdout.write(output + "\n");
|
|
347
|
+
// Exit 1 on HIGH or CRITICAL so CI can gate on it, the same convention
|
|
348
|
+
// `scan --fail-on` already uses.
|
|
349
|
+
if (flags["fail-on-risk"] && ["high", "critical"].includes(result.risk)) process.exitCode = 1;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case "passport": {
|
|
354
|
+
const policyFile = await loadPolicy(flags.policy, cwd);
|
|
355
|
+
const { output, exitCode } = await passportCmd.passport({
|
|
356
|
+
agentId: sub && !sub.startsWith("-") && sub !== "badge" ? sub : null,
|
|
357
|
+
cwd,
|
|
358
|
+
stateDir: stateDirFor(flags, cwd),
|
|
359
|
+
policy: { rules: policyFile.rules, version: policyFile.version ?? null },
|
|
360
|
+
json: Boolean(flags.json),
|
|
361
|
+
sign: Boolean(flags.sign),
|
|
362
|
+
out: flags.out ? String(flags.out) : null,
|
|
363
|
+
badge: Boolean(flags.badge) || sub === "badge",
|
|
364
|
+
badgeOut: flags["badge-out"] ? String(flags["badge-out"]) : null,
|
|
365
|
+
});
|
|
366
|
+
if (output) process.stdout.write(output + "\n");
|
|
367
|
+
if (exitCode) process.exitCode = exitCode;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case "prove": {
|
|
372
|
+
const target = flags.cwd ? cwd : cwd;
|
|
373
|
+
const policyFile = await loadPolicy(flags.policy, target);
|
|
374
|
+
const { output, exitCode } = await proveCmd.prove({
|
|
375
|
+
decisionId: sub,
|
|
376
|
+
cwd: target,
|
|
377
|
+
stateDir: stateDirFor(flags, target),
|
|
378
|
+
// The policy is part of what a proof attests: a decision only means
|
|
379
|
+
// something against the rules that produced it.
|
|
380
|
+
policy: { rules: policyFile.rules, version: policyFile.version ?? null },
|
|
381
|
+
json: Boolean(flags.json),
|
|
382
|
+
out: flags.out ? String(flags.out) : null,
|
|
383
|
+
});
|
|
384
|
+
if (output) process.stdout.write(output + "\n");
|
|
385
|
+
if (exitCode) process.exitCode = exitCode;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
case "verify": {
|
|
390
|
+
const { output, exitCode } = await proveCmd.verify({
|
|
391
|
+
proof: sub,
|
|
392
|
+
publicKey: flags.key ? String(flags.key) : null,
|
|
393
|
+
cwd,
|
|
394
|
+
stateDir: stateDirFor(flags, cwd),
|
|
395
|
+
json: Boolean(flags.json),
|
|
396
|
+
});
|
|
397
|
+
if (output) process.stdout.write(output + "\n");
|
|
398
|
+
// Exit 1 on a failed verification so CI can gate on it. A verifier that
|
|
399
|
+
// always exits 0 is a verifier nobody can automate.
|
|
400
|
+
if (exitCode) process.exitCode = exitCode;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
306
404
|
case "scan": {
|
|
307
|
-
const animateScan = cinematicEnabled({ json: Boolean(flags.json), pace: flags.fast ? 0 : 450 });
|
|
308
405
|
const { result, output } = await scan({
|
|
309
406
|
cwd,
|
|
310
407
|
json: Boolean(flags.json),
|
|
311
408
|
deep: Boolean(flags.deep),
|
|
312
|
-
phase: (label, fn, detail) => runPhase(label, fn, { enabled: animateScan, detail }),
|
|
313
409
|
});
|
|
314
410
|
|
|
315
411
|
// Written before the exit-code gate below, so a failing scan still
|
|
@@ -434,11 +530,32 @@ async function main() {
|
|
|
434
530
|
});
|
|
435
531
|
process.stdin.on("data", (c) => framer.push(c));
|
|
436
532
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
533
|
+
// Premium gateway startup — to stderr so stdout stays JSON-RPC clean.
|
|
534
|
+
{
|
|
535
|
+
const animated = shouldAnimate({ pace: flags.pace ? Number(flags.pace) : 700, json: false });
|
|
536
|
+
const gwRules = daemon?.currentRules().length || rules.length;
|
|
537
|
+
if (animated) {
|
|
538
|
+
try {
|
|
539
|
+
process.stderr.write("\n" + brandHeader({ width: 62 }) + "\n\n");
|
|
540
|
+
} catch {}
|
|
541
|
+
}
|
|
542
|
+
log(`gateway up · ${Object.keys(servers).length} upstream · ${gwRules} rules` + (daemon ? ` · synced with ${apiUrl}` : " · local policy"));
|
|
543
|
+
// Also emit a small protected panel for human visibility (stderr).
|
|
544
|
+
try {
|
|
545
|
+
const gwPanel = panel({
|
|
546
|
+
lines: [
|
|
547
|
+
`${bold("CIRVIX GATEWAY")}`,
|
|
548
|
+
``,
|
|
549
|
+
`${"Upstreams".padEnd(12)} ${Object.keys(servers).length}`,
|
|
550
|
+
`${"Policy".padEnd(12)} ${green(bold("● ENFORCING"))} ${dim(plural(gwRules, "rule"))}`,
|
|
551
|
+
`${"Audit".padEnd(12)} ${green(bold("● RECORDING"))}`,
|
|
552
|
+
`${"Mode".padEnd(12)} ${dim(daemon ? "synced" : "local")}`,
|
|
553
|
+
],
|
|
554
|
+
width: 48,
|
|
555
|
+
});
|
|
556
|
+
process.stderr.write(gwPanel + "\n");
|
|
557
|
+
} catch {}
|
|
558
|
+
}
|
|
442
559
|
|
|
443
560
|
await new Promise((resolve) => {
|
|
444
561
|
let closing = false;
|
|
@@ -538,17 +655,6 @@ async function main() {
|
|
|
538
655
|
if (flags.json) {
|
|
539
656
|
process.stdout.write(JSON.stringify(decision, null, 2) + "\n");
|
|
540
657
|
} else {
|
|
541
|
-
// Human TTY only: the request visibly travels through the boundary
|
|
542
|
-
// before the real verdict prints. Piped/CI output is untouched.
|
|
543
|
-
if (cinematicEnabled({ json: false, pace: flags.fast ? 0 : 350 })) {
|
|
544
|
-
await requestTravel({
|
|
545
|
-
agent: String(flags.agent ?? "local"),
|
|
546
|
-
tool: action,
|
|
547
|
-
target: resource,
|
|
548
|
-
pace: 350,
|
|
549
|
-
animated: true,
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
658
|
const tone =
|
|
553
659
|
decision.verdict === "permit" ? green : decision.verdict === "hold" ? amber : red;
|
|
554
660
|
process.stdout.write(
|
|
@@ -558,6 +664,9 @@ async function main() {
|
|
|
558
664
|
` ${dim("rule")} ${decision.rule ?? dim("— no rule matched (default deny)")}`,
|
|
559
665
|
` ${dim("reason")} ${decision.reason}`,
|
|
560
666
|
decision.remediation ? ` ${dim("fix")} ${blue(decision.remediation)}` : "",
|
|
667
|
+
decision.verdict === "deny" && !flags.json
|
|
668
|
+
? ` ${dim("share")} ${dim(`was this a good catch? ${SHARE_URL} — redact first, your call`)}`
|
|
669
|
+
: "",
|
|
561
670
|
decision.approvers?.length
|
|
562
671
|
? ` ${dim("waits")} ${decision.approvers.join(", ")}`
|
|
563
672
|
: "",
|
|
@@ -590,25 +699,75 @@ async function main() {
|
|
|
590
699
|
return d.verdict === "deny" ? 1 : 0;
|
|
591
700
|
}
|
|
592
701
|
|
|
593
|
-
const
|
|
702
|
+
const isWhyDeny = d.verdict === "deny";
|
|
703
|
+
const isWhyHold = d.verdict === "hold";
|
|
704
|
+
const whyTone = isWhyDeny ? red : isWhyHold ? amber : green;
|
|
705
|
+
const whyDecision = isWhyDeny ? "✕ BLOCKED" : isWhyHold ? "● AWAITING APPROVAL" : "✓ " + String(d.verdict).toUpperCase();
|
|
706
|
+
const whyRisk = String(d.risk ?? "unknown").toUpperCase();
|
|
707
|
+
const whyRiskTone = whyRisk === "CRITICAL" ? red : whyRisk === "HIGH" ? amber : whyRisk === "MEDIUM" ? blue : dim;
|
|
594
708
|
process.stdout.write(
|
|
595
709
|
[
|
|
596
710
|
"",
|
|
597
|
-
` ${
|
|
598
|
-
` ${dim("rule")} ${d.rule ?? dim("— no rule matched (default deny)")}`,
|
|
599
|
-
` ${dim("reason")} ${d.reason ?? dim("—")}`,
|
|
600
|
-
` ${dim("agent")} ${d.agent ?? dim("—")}`,
|
|
601
|
-
` ${dim("when")} ${d.ts}`,
|
|
602
|
-
// The whole point of this command in an incident: it hands you the
|
|
603
|
-
// thread to pull, not just the one bead you arrived holding.
|
|
604
|
-
` ${dim("run")} ${d.runId ? blue(d.runId) : dim("— recorded outside a run")}`,
|
|
711
|
+
` ${bold("CIRVIX DECISION ANALYSIS")}`,
|
|
605
712
|
"",
|
|
713
|
+
` ${dim("Decision".padEnd(12))} ${whyTone(bold(whyDecision))}`,
|
|
714
|
+
` ${dim("Risk".padEnd(12))} ${whyRiskTone(bold(whyRisk))}`,
|
|
715
|
+
"",
|
|
716
|
+
` ${dim("Tool".padEnd(12))} ${bold(String(d.tool ?? d.action ?? "—"))}`,
|
|
717
|
+
d.resource ? ` ${dim("Target".padEnd(12))} ${d.resource}` : "",
|
|
718
|
+
d.destination ? ` ${dim("Destination".padEnd(12))} ${d.destination}` : "",
|
|
719
|
+
` ${dim("Matched policy".padEnd(12))} ${d.rule ?? dim("— no rule matched (default deny)")}`,
|
|
720
|
+
d.reason ? ` ${dim("Reason".padEnd(12))} ${d.reason}` : "",
|
|
721
|
+
` ${dim("Agent".padEnd(12))} ${d.agent ?? dim("—")}`,
|
|
722
|
+
` ${dim("When".padEnd(12))} ${d.ts}`,
|
|
723
|
+
` ${dim("Run".padEnd(12))} ${d.runId ? blue(d.runId) : dim("— recorded outside a run")}`,
|
|
724
|
+
"",
|
|
725
|
+
` ${dim("Decision path")}`,
|
|
726
|
+
` ${dim("secret detection")}`,
|
|
727
|
+
` ${dim("↓")}`,
|
|
728
|
+
` ${dim("risk classification")}`,
|
|
729
|
+
` ${dim("↓")}`,
|
|
730
|
+
` ${dim("policy evaluation")}`,
|
|
731
|
+
` ${dim("↓")}`,
|
|
732
|
+
` ${whyTone(isWhyDeny ? "BLOCK" : isWhyHold ? "HOLD" : "ALLOW")}`,
|
|
733
|
+
"",
|
|
734
|
+
/* The trifecta is the one refusal whose reason lives outside this
|
|
735
|
+
call, so it gets the sequence rendered rather than a rule name.
|
|
736
|
+
"Blocked: trifecta" is indistinguishable from a bug; three
|
|
737
|
+
timestamped steps are something the reader can act on. */
|
|
738
|
+
...(d.trifecta?.complete
|
|
739
|
+
? [
|
|
740
|
+
` ${whyTone(bold("LETHAL TRIFECTA"))} ${dim("all three conditions met in this session")}`,
|
|
741
|
+
"",
|
|
742
|
+
...["sensitive_data", "untrusted_content", "outbound_action"]
|
|
743
|
+
.map((k) => [k, d.trifecta.legs?.[k]])
|
|
744
|
+
.filter(([, v]) => v)
|
|
745
|
+
.sort((a, b) => String(a[1].at ?? "").localeCompare(String(b[1].at ?? "")))
|
|
746
|
+
.map(
|
|
747
|
+
([k, v], i) =>
|
|
748
|
+
` ${bold(String(i + 1) + ".")} ${dim(k.replace(/_/g, " ").padEnd(18))} ${v.why}` +
|
|
749
|
+
(v.at ? `
|
|
750
|
+
${dim(v.at)}` : ""),
|
|
751
|
+
),
|
|
752
|
+
"",
|
|
753
|
+
` ${dim("Cirvix refuses on capability and opportunity. It does not claim the")}`,
|
|
754
|
+
` ${dim("sensitive bytes are in this request — that needs data-flow analysis")}`,
|
|
755
|
+
` ${dim("it deliberately does not do.")}`,
|
|
756
|
+
"",
|
|
757
|
+
]
|
|
758
|
+
: d.trifecta?.satisfied?.length
|
|
759
|
+
? [
|
|
760
|
+
` ${dim("trifecta")} ${d.trifecta.satisfied.length} of 3 conditions met` +
|
|
761
|
+
(d.trifecta.imminent ? ` ${whyRiskTone("one step from complete")}` : ""),
|
|
762
|
+
"",
|
|
763
|
+
]
|
|
764
|
+
: []),
|
|
606
765
|
...(d.considered?.length
|
|
607
766
|
? [
|
|
608
|
-
` ${dim("considered")}`,
|
|
767
|
+
` ${dim("considered")} ${dim(`${d.considered.filter((c) => c.matched).length} of ${d.considered.length} matched`)}`,
|
|
609
768
|
...d.considered.map(
|
|
610
769
|
(c) =>
|
|
611
|
-
` ${c.matched ? bold("→") : dim(" ")} ${dim(String(c.effect).padEnd(
|
|
770
|
+
` ${c.matched ? bold("→") : dim(" ")} ${dim(String(c.effect).padEnd(11))} ${c.matched ? c.rule : dim(c.rule)}`,
|
|
612
771
|
),
|
|
613
772
|
"",
|
|
614
773
|
]
|
|
@@ -690,11 +849,43 @@ async function main() {
|
|
|
690
849
|
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
691
850
|
return res.ok ? 0 : 1;
|
|
692
851
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
852
|
+
if (res.ok) {
|
|
853
|
+
const W = 62;
|
|
854
|
+
const top = ` ${dim(`╭─ CIRVIX AUDIT VERIFICATION ${"─".repeat(Math.max(0, W - 26))}╮`)}`;
|
|
855
|
+
const bottom = ` ${dim(`╰${"─".repeat(W)}╯`)}`;
|
|
856
|
+
const chainLines = [
|
|
857
|
+
``,
|
|
858
|
+
` ${green("✓")} ${dim("Hash chain intact")}`,
|
|
859
|
+
` ${green("✓")} ${dim(`${res.records} records verified`)}`,
|
|
860
|
+
` ${green("✓")} ${dim("No records altered")}`,
|
|
861
|
+
``,
|
|
862
|
+
` ${bold("CHAIN")}`,
|
|
863
|
+
``,
|
|
864
|
+
` ${dim("current")}`,
|
|
865
|
+
` ${dim("↓")}`,
|
|
866
|
+
` ${dim("previous")}`,
|
|
867
|
+
` ${dim("↓")}`,
|
|
868
|
+
` ${dim("previous")}`,
|
|
869
|
+
` ${dim("↓")}`,
|
|
870
|
+
` ${dim("genesis")}`,
|
|
871
|
+
``,
|
|
872
|
+
` ${bold("STATUS")} ${green(bold("● INTEGRITY OK"))}`,
|
|
873
|
+
``,
|
|
874
|
+
` ${dim(`head ${String(res.head ?? "").slice(0, 16)}…`)}`,
|
|
875
|
+
``,
|
|
876
|
+
` ${dim("Verification proves records were not altered after they were written.")}`,
|
|
877
|
+
` ${dim("It does not attest to their content.")}`,
|
|
878
|
+
``,
|
|
879
|
+
];
|
|
880
|
+
process.stdout.write(`\n${top}\n`);
|
|
881
|
+
process.stdout.write(`\n ${bold("CIRVIX AUDIT VERIFICATION")}\n`);
|
|
882
|
+
for (const l of chainLines) process.stdout.write(l + "\n");
|
|
883
|
+
process.stdout.write(`${bottom}\n\n`);
|
|
884
|
+
} else {
|
|
885
|
+
process.stdout.write(
|
|
886
|
+
`\n ${red(bold("chain broken"))} ${dim(`at record ${res.brokenAt} of ${res.records}`)}\n ${res.reason}\n\n`,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
698
889
|
return res.ok ? 0 : 1;
|
|
699
890
|
}
|
|
700
891
|
|
|
@@ -742,19 +933,6 @@ async function main() {
|
|
|
742
933
|
process.stderr.write(red(" explain needs --tool <name>.\n"));
|
|
743
934
|
return 2;
|
|
744
935
|
}
|
|
745
|
-
const callArgs = callArgsFrom(flags);
|
|
746
|
-
// Human TTY only: watch the request travel through the boundary
|
|
747
|
-
// before the real verdict prints. Piped/CI output is untouched.
|
|
748
|
-
if (cinematicEnabled({ json: Boolean(flags.json), pace: flags.fast ? 0 : 350 })) {
|
|
749
|
-
const target = callArgs.path ?? callArgs.url ?? callArgs.command ?? "";
|
|
750
|
-
await requestTravel({
|
|
751
|
-
agent: String(flags.agent ?? "local"),
|
|
752
|
-
tool,
|
|
753
|
-
target: String(target ?? ""),
|
|
754
|
-
pace: 350,
|
|
755
|
-
animated: true,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
936
|
const { output, code } = await policyCmd.explain({
|
|
759
937
|
path: loaded.path,
|
|
760
938
|
// The starter set has no file, so explain against the rules directly.
|
|
@@ -762,7 +940,7 @@ async function main() {
|
|
|
762
940
|
cwd,
|
|
763
941
|
json: Boolean(flags.json),
|
|
764
942
|
tool,
|
|
765
|
-
args:
|
|
943
|
+
args: callArgsFrom(flags),
|
|
766
944
|
agent: String(flags.agent ?? "local"),
|
|
767
945
|
environment: String(flags.env ?? "local"),
|
|
768
946
|
});
|
|
@@ -770,6 +948,22 @@ async function main() {
|
|
|
770
948
|
return code;
|
|
771
949
|
}
|
|
772
950
|
|
|
951
|
+
case "simulate": {
|
|
952
|
+
const { simulatePolicy } = await import("../src/commands/simulate.mjs");
|
|
953
|
+
const { output, code } = await simulatePolicy({
|
|
954
|
+
rules: loaded.rules,
|
|
955
|
+
action: flags.action ?? flags.tool ?? "fs:read",
|
|
956
|
+
resource: flags.resource ?? flags.path ?? flags.command ?? "",
|
|
957
|
+
tool: flags.tool ?? "file_reader",
|
|
958
|
+
intent: flags.intent ?? null,
|
|
959
|
+
agent: String(flags.agent ?? "local"),
|
|
960
|
+
json: Boolean(flags.json),
|
|
961
|
+
cwd,
|
|
962
|
+
});
|
|
963
|
+
process.stdout.write(output + "\n");
|
|
964
|
+
return code;
|
|
965
|
+
}
|
|
966
|
+
|
|
773
967
|
case "list":
|
|
774
968
|
default: {
|
|
775
969
|
const { output, code } = policyCmd.list(loaded.rules, {
|
|
@@ -789,11 +983,76 @@ async function main() {
|
|
|
789
983
|
cwd,
|
|
790
984
|
json: Boolean(flags.json),
|
|
791
985
|
force: Boolean(flags.force),
|
|
986
|
+
apply: Boolean(flags.apply),
|
|
987
|
+
dryRun: Boolean(flags["dry-run"]),
|
|
988
|
+
rollback: flags.rollback ? (typeof flags.rollback === "string" ? flags.rollback : true) : false,
|
|
792
989
|
});
|
|
793
990
|
process.stdout.write(output + "\n");
|
|
794
991
|
return result.ok ? 0 : 1;
|
|
795
992
|
}
|
|
796
993
|
|
|
994
|
+
/* ------------------------------------------------------------ simulate */
|
|
995
|
+
case "simulate": {
|
|
996
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
997
|
+
const { simulatePolicy } = await import("../src/commands/simulate.mjs");
|
|
998
|
+
const { output, code } = await simulatePolicy({
|
|
999
|
+
rules,
|
|
1000
|
+
action: flags.action ?? flags.tool ?? positional[1] ?? "fs:read",
|
|
1001
|
+
resource: flags.resource ?? flags.path ?? flags.command ?? positional[2] ?? "",
|
|
1002
|
+
tool: flags.tool ?? "file_reader",
|
|
1003
|
+
intent: flags.intent ?? null,
|
|
1004
|
+
agent: String(flags.agent ?? "local"),
|
|
1005
|
+
json: Boolean(flags.json),
|
|
1006
|
+
cwd,
|
|
1007
|
+
});
|
|
1008
|
+
process.stdout.write(output + "\n");
|
|
1009
|
+
return code;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/* ----------------------------------------------------------------- kill */
|
|
1013
|
+
case "kill": {
|
|
1014
|
+
const { executeKillCommand } = await import("../src/commands/kill.mjs");
|
|
1015
|
+
const { output, code } = await executeKillCommand({
|
|
1016
|
+
scope: flags.scope ?? "agent",
|
|
1017
|
+
target: positional[1] ?? flags.target ?? null,
|
|
1018
|
+
reason: flags.reason ?? "Emergency freeze triggered via CLI",
|
|
1019
|
+
release: flags.release ?? null,
|
|
1020
|
+
list: Boolean(flags.list),
|
|
1021
|
+
json: Boolean(flags.json),
|
|
1022
|
+
});
|
|
1023
|
+
process.stdout.write(output + "\n");
|
|
1024
|
+
return code;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/* --------------------------------------------------------------- shadow */
|
|
1028
|
+
case "shadow": {
|
|
1029
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
1030
|
+
const { executeShadowCommand } = await import("../src/commands/shadow.mjs");
|
|
1031
|
+
const { output, code } = await executeShadowCommand({
|
|
1032
|
+
rules,
|
|
1033
|
+
action: flags.action ?? positional[1] ?? null,
|
|
1034
|
+
resource: flags.resource ?? positional[2] ?? null,
|
|
1035
|
+
json: Boolean(flags.json),
|
|
1036
|
+
cwd,
|
|
1037
|
+
});
|
|
1038
|
+
process.stdout.write(output + "\n");
|
|
1039
|
+
return code;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* -------------------------------------------------------------- redteam */
|
|
1043
|
+
case "redteam": {
|
|
1044
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
1045
|
+
const { executeRedTeamCommand } = await import("../src/commands/redteam.mjs");
|
|
1046
|
+
const { output, code } = await executeRedTeamCommand({
|
|
1047
|
+
rules,
|
|
1048
|
+
plugins: flags.plugins ? String(flags.plugins).split(",") : null,
|
|
1049
|
+
json: Boolean(flags.json),
|
|
1050
|
+
cwd,
|
|
1051
|
+
});
|
|
1052
|
+
process.stdout.write(output + "\n");
|
|
1053
|
+
return code;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
797
1056
|
/* -------------------------------------------------------------- status */
|
|
798
1057
|
case "upgrade": {
|
|
799
1058
|
// `positional` already has the command at [0]; the rest are the
|
|
@@ -806,18 +1065,65 @@ async function main() {
|
|
|
806
1065
|
|
|
807
1066
|
case "status": {
|
|
808
1067
|
const rules = await loadRules(flags.policy, cwd);
|
|
809
|
-
const animateStatus = cinematicEnabled({ json: Boolean(flags.json), pace: flags.fast ? 0 : 450 });
|
|
810
1068
|
const { output } = await statusCmd({
|
|
811
1069
|
cwd,
|
|
812
1070
|
rules,
|
|
813
1071
|
json: Boolean(flags.json),
|
|
814
1072
|
stateDir: stateDirFor(flags, cwd),
|
|
815
|
-
progress: ttyProgress({ enabled: animateStatus }),
|
|
816
1073
|
});
|
|
817
1074
|
process.stdout.write(output + "\n");
|
|
818
1075
|
return 0;
|
|
819
1076
|
}
|
|
820
1077
|
|
|
1078
|
+
/* ------------------------------------------------------------ console */
|
|
1079
|
+
case "console": {
|
|
1080
|
+
if (typeof flags.theme === "string") {
|
|
1081
|
+
const { setTheme } = await import("../src/core/theme.mjs");
|
|
1082
|
+
try {
|
|
1083
|
+
setTheme(flags.theme);
|
|
1084
|
+
process.env.CIRVIX_THEME = flags.theme;
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
process.stderr.write(red(` ${err.message}\n`));
|
|
1087
|
+
return 2;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
1091
|
+
const { consoleCmd } = await import("../src/commands/console.mjs");
|
|
1092
|
+
await consoleCmd({
|
|
1093
|
+
cwd,
|
|
1094
|
+
rules,
|
|
1095
|
+
mode: flags.mode === "audit" ? MODE.AUDIT : MODE.ENFORCE,
|
|
1096
|
+
evalText: typeof flags.eval === "string" ? flags.eval : null,
|
|
1097
|
+
once: Boolean(flags.once ?? flags.eval),
|
|
1098
|
+
});
|
|
1099
|
+
return 0;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
case "theme": {
|
|
1103
|
+
const { setTheme, THEME_NAMES } = await import("../src/core/theme.mjs");
|
|
1104
|
+
const name = sub ?? flags.set;
|
|
1105
|
+
if (!name) {
|
|
1106
|
+
process.stdout.write(
|
|
1107
|
+
`\n Current theme: ${process.env.CIRVIX_THEME ?? "dark"}\n Available: ${THEME_NAMES.join(", ")}\n\n Usage: cirvix theme <name>\n Persist it: CIRVIX_THEME=${THEME_NAMES[0]} cirvix console\n\n`,
|
|
1108
|
+
);
|
|
1109
|
+
return 0;
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
setTheme(name);
|
|
1113
|
+
process.stdout.write(`\n Theme → ${name} (set CIRVIX_THEME=${name} to keep it)\n\n`);
|
|
1114
|
+
return 0;
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
process.stderr.write(red(` ${err.message}\n`));
|
|
1117
|
+
return 2;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
case "onboard": {
|
|
1122
|
+
const { onboard } = await import("../src/commands/onboard.mjs");
|
|
1123
|
+
await onboard({ cwd, pace: flags.fast ? 0 : 400 });
|
|
1124
|
+
return 0;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
821
1127
|
/* ---------------------------------------------------------------- demo */
|
|
822
1128
|
case "demo": {
|
|
823
1129
|
const rules = flags.policy ? await loadRules(flags.policy, cwd) : null;
|
|
@@ -837,6 +1143,66 @@ async function main() {
|
|
|
837
1143
|
case "logs": {
|
|
838
1144
|
const stateDir = stateDirFor(flags, cwd);
|
|
839
1145
|
const file = String(flags.file ?? join(stateDir, "audit.jsonl"));
|
|
1146
|
+
|
|
1147
|
+
// Live mode: cirvix logs --watch
|
|
1148
|
+
if (flags.watch || flags.follow || flags.w) {
|
|
1149
|
+
if (flags.json) {
|
|
1150
|
+
process.stderr.write(red(" --watch is not compatible with --json.\n"));
|
|
1151
|
+
return 2;
|
|
1152
|
+
}
|
|
1153
|
+
const { watch } = await import("node:fs");
|
|
1154
|
+
const live = new LiveStream({ stream: process.stdout, title: "CIRVIX LIVE · protection active" });
|
|
1155
|
+
// Print existing tail first
|
|
1156
|
+
const existing = await journal.read(file);
|
|
1157
|
+
const tail = journal.query(existing, {
|
|
1158
|
+
last: flags.last ? Number(flags.last) : 10,
|
|
1159
|
+
risk: typeof flags.risk === "string" ? flags.risk : undefined,
|
|
1160
|
+
decision: typeof flags.decision === "string" ? flags.decision : undefined,
|
|
1161
|
+
});
|
|
1162
|
+
live.header();
|
|
1163
|
+
for (const r of tail) live.push(r);
|
|
1164
|
+
if (tail.length === 0) {
|
|
1165
|
+
process.stdout.write(` ${dim("waiting for decisions…")} ${dim(`tailing ${file}`)}\n`);
|
|
1166
|
+
}
|
|
1167
|
+
// Watch for new records — polling via fs.watch where available, fallback to interval.
|
|
1168
|
+
let known = existing.length;
|
|
1169
|
+
let watcher = null;
|
|
1170
|
+
let polling = null;
|
|
1171
|
+
const emitNew = async () => {
|
|
1172
|
+
const all = await journal.read(file);
|
|
1173
|
+
if (all.length > known) {
|
|
1174
|
+
const fresh = all.slice(known);
|
|
1175
|
+
const filtered = journal.query(fresh, {
|
|
1176
|
+
risk: typeof flags.risk === "string" ? flags.risk : undefined,
|
|
1177
|
+
decision: typeof flags.decision === "string" ? flags.decision : undefined,
|
|
1178
|
+
agent: typeof flags.agent === "string" ? flags.agent : undefined,
|
|
1179
|
+
tool: typeof flags.tool === "string" ? flags.tool : undefined,
|
|
1180
|
+
deniedOnly: Boolean(flags.denied),
|
|
1181
|
+
});
|
|
1182
|
+
for (const r of filtered) live.push(r);
|
|
1183
|
+
known = all.length;
|
|
1184
|
+
} else if (all.length < known) {
|
|
1185
|
+
known = all.length;
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
try {
|
|
1189
|
+
watcher = watch(file, async () => { await emitNew().catch(() => {}); });
|
|
1190
|
+
} catch {
|
|
1191
|
+
polling = setInterval(() => void emitNew(), 700);
|
|
1192
|
+
}
|
|
1193
|
+
if (!watcher) polling = setInterval(() => void emitNew(), 700);
|
|
1194
|
+
await new Promise((resolve) => {
|
|
1195
|
+
const done = () => {
|
|
1196
|
+
try { watcher?.close(); } catch {}
|
|
1197
|
+
if (polling) clearInterval(polling);
|
|
1198
|
+
resolve();
|
|
1199
|
+
};
|
|
1200
|
+
process.on("SIGINT", done);
|
|
1201
|
+
process.on("SIGTERM", done);
|
|
1202
|
+
});
|
|
1203
|
+
return 0;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
840
1206
|
const records = await journal.read(file);
|
|
841
1207
|
|
|
842
1208
|
// `--tree <id>` prints one decision in full rather than the list.
|
|
@@ -916,20 +1282,29 @@ async function main() {
|
|
|
916
1282
|
process.stdout.write("\n " + bold(plural(pending.length, "call")) + dim(" waiting\n\n"));
|
|
917
1283
|
for (const a of pending) {
|
|
918
1284
|
const riskTone = { low: dim, medium: blue, high: amber, critical: red }[a.risk] ?? dim;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
)
|
|
922
|
-
process.stdout.write(
|
|
923
|
-
process.stdout.write(
|
|
924
|
-
|
|
925
|
-
);
|
|
1285
|
+
const W = 62;
|
|
1286
|
+
const top = ` ${dim(`╭─ HUMAN APPROVAL REQUIRED ${"─".repeat(Math.max(0, W - 28))}╮`)}`;
|
|
1287
|
+
const bottom = ` ${dim(`╰${"─".repeat(W)}╯`)}`;
|
|
1288
|
+
process.stdout.write(top + "\n");
|
|
1289
|
+
process.stdout.write(` ${dim("│")} ${dim("Agent".padEnd(10))} ${a.agent ?? "—"} ${dim("│")}\n`);
|
|
1290
|
+
process.stdout.write(` ${dim("│")} ${dim("Action".padEnd(10))} ${a.tool ?? "—"} ${dim("│")}\n`);
|
|
1291
|
+
process.stdout.write(` ${dim("│")} ${dim("Target".padEnd(10))} ${String(a.resource ?? "").slice(0, 32).padEnd(32)} ${dim("│")}\n`);
|
|
1292
|
+
process.stdout.write(` ${dim("│")} ${"".padEnd(46)} ${dim("│")}\n`);
|
|
1293
|
+
process.stdout.write(` ${dim("│")} ${dim("Risk".padEnd(10))} ${riskTone(String(a.risk ?? "").toUpperCase().padEnd(9))} ${dim("│")}\n`);
|
|
1294
|
+
process.stdout.write(` ${dim("│")} ${dim("Policy".padEnd(10))} ${String(a.rule ?? "—").slice(0, 32).padEnd(32)} ${dim("│")}\n`);
|
|
1295
|
+
process.stdout.write(` ${dim("│")} ${dim("Waits on".padEnd(10))} ${(a.approvers ?? []).join(", ") || "—"} ${dim("│")}\n`);
|
|
1296
|
+
process.stdout.write(` ${dim("│")} ${"".padEnd(46)} ${dim("│")}\n`);
|
|
1297
|
+
process.stdout.write(` ${dim("│")} ${dim("Reason")} ${dim("│")}\n`);
|
|
1298
|
+
process.stdout.write(` ${dim("│")} ${(a.reason ?? "Production database mutation requires human authorization.").slice(0, 44).padEnd(44)} ${dim("│")}\n`);
|
|
1299
|
+
process.stdout.write(` ${dim("│")} ${"".padEnd(46)} ${dim("│")}\n`);
|
|
1300
|
+
process.stdout.write(` ${dim("│")} ${green("[A] Approve")} ${dim(" ")} ${red("[R] Reject")} ${dim(` ${a.id}`)} ${dim("│")}\n`);
|
|
1301
|
+
process.stdout.write(bottom + "\n\n");
|
|
926
1302
|
if (a.state !== "pending") {
|
|
927
|
-
process.stdout.write(` ${dim("state")} ${a.state}${a.decidedBy ? dim(` by ${a.decidedBy}`) : ""}\n`);
|
|
1303
|
+
process.stdout.write(` ${dim("state")} ${a.state}${a.decidedBy ? dim(` by ${a.decidedBy}`) : ""}\n\n`);
|
|
928
1304
|
}
|
|
929
|
-
process.stdout.write("\n");
|
|
930
1305
|
}
|
|
931
1306
|
process.stdout.write(
|
|
932
|
-
` ${dim("Decide one:")} ${blue(`cirvix approve ${pending[0].id} --by you@example.com`)}\n\n`,
|
|
1307
|
+
` ${dim("Decide one:")} ${blue(`cirvix approve ${pending[0].id} --by you@example.com`)} ${dim("or")} ${red(`cirvix deny ${pending[0].id} --by you@example.com`)}\n\n`,
|
|
933
1308
|
);
|
|
934
1309
|
return 0;
|
|
935
1310
|
}
|
|
@@ -1020,6 +1395,10 @@ async function main() {
|
|
|
1020
1395
|
meter: runtimeMeter,
|
|
1021
1396
|
write: (s) => process.stderr.write(s),
|
|
1022
1397
|
});
|
|
1398
|
+
/* Measured, not asserted. The panel below reports these, and the only
|
|
1399
|
+
honest source for them is the decision stream itself. */
|
|
1400
|
+
const counters = { blocked: 0, approvals: 0, violations: 0 };
|
|
1401
|
+
const seenAgents = new Set();
|
|
1023
1402
|
const pipeline = new Pipeline({
|
|
1024
1403
|
rules,
|
|
1025
1404
|
cwd,
|
|
@@ -1033,7 +1412,14 @@ async function main() {
|
|
|
1033
1412
|
meter: runtimeMeter,
|
|
1034
1413
|
agents: new AgentRegistry(),
|
|
1035
1414
|
onEvent: (e) => {
|
|
1036
|
-
if (e.kind === "decision")
|
|
1415
|
+
if (e.kind === "decision") {
|
|
1416
|
+
notice(e);
|
|
1417
|
+
const ev = e.event ?? e;
|
|
1418
|
+
if (ev.agent) seenAgents.add(ev.agent);
|
|
1419
|
+
if (ev.decision === "deny") counters.blocked += 1;
|
|
1420
|
+
else if (ev.decision === "require_approval") counters.approvals += 1;
|
|
1421
|
+
if (ev.risk === "critical") counters.violations += 1;
|
|
1422
|
+
}
|
|
1037
1423
|
},
|
|
1038
1424
|
log: (m) => process.stderr.write(`[cirvix] ${m}\n`),
|
|
1039
1425
|
});
|
|
@@ -1059,14 +1445,70 @@ async function main() {
|
|
|
1059
1445
|
});
|
|
1060
1446
|
await server.start();
|
|
1061
1447
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1448
|
+
// Premium startup sequence — brand + real state, zero fake.
|
|
1449
|
+
const runtimeAnimated = shouldAnimate({ pace: flags.pace ? Number(flags.pace) : 700, json: Boolean(flags.json) });
|
|
1450
|
+
// Compute policy tests count for display (real).
|
|
1451
|
+
let rtTests = 0;
|
|
1452
|
+
try {
|
|
1453
|
+
const { loadPolicyFile } = await import("../src/commands/policy.mjs");
|
|
1454
|
+
let policyPath = null;
|
|
1455
|
+
for (const cand of ["cirvix.policy", "cirvix.policy.json", ".cirvix/policy.json"]) {
|
|
1456
|
+
const p = join(cwd, cand);
|
|
1457
|
+
try { await access(p); policyPath = p; break; } catch {}
|
|
1458
|
+
}
|
|
1459
|
+
if (policyPath) {
|
|
1460
|
+
const loaded = await loadPolicyFile(policyPath, { cwd });
|
|
1461
|
+
rtTests = loaded.tests?.length ?? 0;
|
|
1462
|
+
}
|
|
1463
|
+
} catch {}
|
|
1464
|
+
if (!flags.json) {
|
|
1465
|
+
// Brand header only when interactive; in CI/non-TTY just show compact.
|
|
1466
|
+
if (runtimeAnimated) {
|
|
1467
|
+
process.stdout.write("\n" + brandHeader({ width: 62 }) + "\n\n");
|
|
1468
|
+
} else {
|
|
1469
|
+
process.stdout.write(`\n ${bold("CIRVIX")} ${dim("· runtime governance")}\n\n`);
|
|
1470
|
+
}
|
|
1471
|
+
process.stdout.write(` ${dim("Initializing CIRVIX runtime...")}\n`);
|
|
1472
|
+
process.stdout.write(` ${green("✓")} ${dim("Control socket established")} ${dim(endpoint)}\n`);
|
|
1473
|
+
process.stdout.write(` ${green("✓")} ${dim("Policy engine loaded")}\n`);
|
|
1474
|
+
process.stdout.write(` ${green("✓")} ${dim("Secret protection enabled")}\n`);
|
|
1475
|
+
process.stdout.write(` ${green("✓")} ${dim("Audit chain initialized")}\n`);
|
|
1476
|
+
process.stdout.write(` ${green("✓")} ${dim(`${plural(rules.length, "rule")} loaded`)}${rtTests ? dim(` · ${rtTests} policy tests`) : ""}\n`);
|
|
1477
|
+
process.stdout.write("\n");
|
|
1478
|
+
// The agent this runtime was started for, plus any that have since
|
|
1479
|
+
// announced themselves. One at startup is a fact, not a placeholder —
|
|
1480
|
+
// but only because it is counted.
|
|
1481
|
+
const agentsSeen = Math.max(seenAgents.size, 1);
|
|
1482
|
+
const protectedLines = [
|
|
1483
|
+
`${bold("CIRVIX PROTECTED")}`,
|
|
1484
|
+
``,
|
|
1485
|
+
`${"Runtime".padEnd(12)} ${green(bold("● ONLINE"))} ${dim(mode === MODE.AUDIT ? "AUDIT · recording only" : "ENFORCING")}`,
|
|
1486
|
+
`${"Policy".padEnd(12)} ${green(bold("● ENFORCING"))} ${dim(plural(rules.length, "rule"))}`,
|
|
1487
|
+
`${"Secrets".padEnd(12)} ${green(bold("● PROTECTED"))}`,
|
|
1488
|
+
`${"Audit".padEnd(12)} ${green(bold("● RECORDING"))}`,
|
|
1489
|
+
`${"Agents".padEnd(12)} ${dim(plural(agentsSeen, "detected", "detected"))}`,
|
|
1490
|
+
``,
|
|
1491
|
+
/* These were string literals — `1 detected`, `0 blocked · 0
|
|
1492
|
+
approvals · 0 violations`. They happened to be true at startup,
|
|
1493
|
+
which is exactly what makes that kind of line dangerous: it reads
|
|
1494
|
+
as measurement, it survives review, and it is wrong the moment
|
|
1495
|
+
anything happens. Counted from the pipeline now. */
|
|
1496
|
+
`${dim(`${counters.blocked} blocked · ${counters.approvals} approvals · ${counters.violations} violations`)}`,
|
|
1497
|
+
];
|
|
1498
|
+
process.stdout.write(panel({ lines: protectedLines, width: 62 }) + "\n\n");
|
|
1499
|
+
process.stdout.write(` ${dim("Ready. Your agent is under policy control.")}\n`);
|
|
1500
|
+
process.stdout.write(` ${dim(`token in ${join(stateDir, "socket.token")}`)}\n\n`);
|
|
1501
|
+
if (mode === MODE.AUDIT) {
|
|
1502
|
+
process.stdout.write(` ${amber(bold("AUDIT MODE"))} ${dim("— decisions are recorded and nothing is blocked.")}\n\n`);
|
|
1503
|
+
}
|
|
1504
|
+
} else {
|
|
1067
1505
|
process.stdout.write(
|
|
1068
|
-
|
|
1506
|
+
`\n ${green(bold("runtime up"))} ${dim(`${plural(rules.length, "rule")} · ${mode} · ${endpoint}`)}\n` +
|
|
1507
|
+
` ${dim(`token in ${join(stateDir, "socket.token")}`)}\n\n`,
|
|
1069
1508
|
);
|
|
1509
|
+
if (mode === MODE.AUDIT) {
|
|
1510
|
+
process.stdout.write(` ${amber(bold("AUDIT MODE"))} ${dim("— decisions are recorded and nothing is blocked.")}\n\n`);
|
|
1511
|
+
}
|
|
1070
1512
|
}
|
|
1071
1513
|
|
|
1072
1514
|
await new Promise((resolve) => {
|
|
@@ -1084,6 +1526,18 @@ async function main() {
|
|
|
1084
1526
|
return 0;
|
|
1085
1527
|
}
|
|
1086
1528
|
|
|
1529
|
+
case "doctor": {
|
|
1530
|
+
return doctor({ cwd, json: Boolean(flags.json) });
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
case "login": {
|
|
1534
|
+
return login({ key: flags.key ? String(flags.key) : null, url: flags.url ? String(flags.url) : null, status: Boolean(flags.status), browser: flags.browser === undefined ? undefined : Boolean(flags.browser), json: Boolean(flags.json) });
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
case "logout": {
|
|
1538
|
+
return logout({ json: Boolean(flags.json) });
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1087
1541
|
case "help":
|
|
1088
1542
|
default:
|
|
1089
1543
|
process.stdout.write(HELP + "\n");
|