@cirvix_ai/agent-control 0.1.2 → 0.1.5
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 +488 -40
- 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/demo.mjs +56 -70
- 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/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 +6 -4
- package/src/commands/shadow.mjs +62 -0
- package/src/commands/simulate.mjs +96 -0
- package/src/commands/status.mjs +121 -36
- package/src/commands/upgrade.mjs +17 -9
- 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 +6 -0
- package/src/core/escape-benchmark.mjs +597 -0
- package/src/core/evidence.mjs +212 -0
- package/src/core/format.mjs +27 -0
- 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/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 +25 -0
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,12 +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";
|
|
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";
|
|
36
46
|
|
|
37
47
|
/**
|
|
38
48
|
* Read from the manifest, never written down twice.
|
|
@@ -57,9 +67,18 @@ const HELP = `
|
|
|
57
67
|
|
|
58
68
|
${bold("GETTING STARTED")}
|
|
59
69
|
init Detect agents and MCP servers, write a policy, start protecting
|
|
70
|
+
init --apply Safely wire detected agents with pre-integration backup
|
|
71
|
+
init --dry-run Preview agent configuration changes without modifying files
|
|
72
|
+
init --rollback [id] Revert agent configurations to pre-integration state
|
|
60
73
|
status Runtime, policy, servers, blocked, approvals, P99 overhead
|
|
74
|
+
doctor diagnose this installation: policy, state, daemon, control plane
|
|
75
|
+
login / logout link this machine to your CIRVIX control plane (browser or --key)
|
|
61
76
|
upgrade Today's usage against your plan, and what lifts the limit
|
|
62
77
|
demo Watch an injected exfiltration attempt get stopped, live
|
|
78
|
+
protect [path] Discover, analyse, apply policy and prove it decides
|
|
79
|
+
passport [agent] What an agent is, by what it has actually done
|
|
80
|
+
prove <decision-id> Sign a decision into a portable proof artifact
|
|
81
|
+
verify <proof> Check a proof offline: signature, chain, integrity
|
|
63
82
|
scan Inventory what is ungoverned on this machine
|
|
64
83
|
|
|
65
84
|
${bold("ENFORCEMENT")}
|
|
@@ -284,7 +303,101 @@ async function main() {
|
|
|
284
303
|
return 0;
|
|
285
304
|
}
|
|
286
305
|
|
|
306
|
+
// BARE `cirvix`:
|
|
307
|
+
// first run in a workspace -> onboarding (what CIRVIX is, three commands)
|
|
308
|
+
// returning, interactive -> the full live terminal
|
|
309
|
+
// returning, piped/CI -> the measured digest + next steps (no animation)
|
|
310
|
+
if (positional.length === 0 && !flags.json && !flags.help) {
|
|
311
|
+
let firstRun = false;
|
|
312
|
+
try { await access(join(cwd, ".cirvix")); } catch { firstRun = true; }
|
|
313
|
+
if (!firstRun) {
|
|
314
|
+
const { canLaunchInteractive } = await import("../src/commands/interactive.mjs");
|
|
315
|
+
if (canLaunchInteractive(flags, positional)) {
|
|
316
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
317
|
+
const { interactive } = await import("../src/commands/interactive.mjs");
|
|
318
|
+
await interactive({ cwd, flags, rules });
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
await welcome({ cwd });
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
287
326
|
switch (command) {
|
|
327
|
+
case "protect": {
|
|
328
|
+
/* Shares policy resolution with `runtime` and `gateway`. A protect that
|
|
329
|
+
read policy differently from the runtime would be proving a decision
|
|
330
|
+
the runtime will not make. */
|
|
331
|
+
const target = sub && !sub.startsWith("-") ? resolve(cwd, sub) : cwd;
|
|
332
|
+
const rules = await loadRules(flags.policy, target);
|
|
333
|
+
const { result, output } = await protectCmd.protect({
|
|
334
|
+
cwd: target,
|
|
335
|
+
rules,
|
|
336
|
+
agent: String(flags.agent ?? "local"),
|
|
337
|
+
environment: String(flags.env ?? "local"),
|
|
338
|
+
json: Boolean(flags.json),
|
|
339
|
+
pace: flags.fast ? 0 : Number(flags.pace ?? 90),
|
|
340
|
+
animate: flags["no-animation"] ? false : undefined,
|
|
341
|
+
stateDir: stateDirFor(flags, target),
|
|
342
|
+
});
|
|
343
|
+
if (output) process.stdout.write(output + "\n");
|
|
344
|
+
// Exit 1 on HIGH or CRITICAL so CI can gate on it, the same convention
|
|
345
|
+
// `scan --fail-on` already uses.
|
|
346
|
+
if (flags["fail-on-risk"] && ["high", "critical"].includes(result.risk)) process.exitCode = 1;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case "passport": {
|
|
351
|
+
const policyFile = await loadPolicy(flags.policy, cwd);
|
|
352
|
+
const { output, exitCode } = await passportCmd.passport({
|
|
353
|
+
agentId: sub && !sub.startsWith("-") && sub !== "badge" ? sub : null,
|
|
354
|
+
cwd,
|
|
355
|
+
stateDir: stateDirFor(flags, cwd),
|
|
356
|
+
policy: { rules: policyFile.rules, version: policyFile.version ?? null },
|
|
357
|
+
json: Boolean(flags.json),
|
|
358
|
+
sign: Boolean(flags.sign),
|
|
359
|
+
out: flags.out ? String(flags.out) : null,
|
|
360
|
+
badge: Boolean(flags.badge) || sub === "badge",
|
|
361
|
+
badgeOut: flags["badge-out"] ? String(flags["badge-out"]) : null,
|
|
362
|
+
});
|
|
363
|
+
if (output) process.stdout.write(output + "\n");
|
|
364
|
+
if (exitCode) process.exitCode = exitCode;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
case "prove": {
|
|
369
|
+
const target = flags.cwd ? cwd : cwd;
|
|
370
|
+
const policyFile = await loadPolicy(flags.policy, target);
|
|
371
|
+
const { output, exitCode } = await proveCmd.prove({
|
|
372
|
+
decisionId: sub,
|
|
373
|
+
cwd: target,
|
|
374
|
+
stateDir: stateDirFor(flags, target),
|
|
375
|
+
// The policy is part of what a proof attests: a decision only means
|
|
376
|
+
// something against the rules that produced it.
|
|
377
|
+
policy: { rules: policyFile.rules, version: policyFile.version ?? null },
|
|
378
|
+
json: Boolean(flags.json),
|
|
379
|
+
out: flags.out ? String(flags.out) : null,
|
|
380
|
+
});
|
|
381
|
+
if (output) process.stdout.write(output + "\n");
|
|
382
|
+
if (exitCode) process.exitCode = exitCode;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
case "verify": {
|
|
387
|
+
const { output, exitCode } = await proveCmd.verify({
|
|
388
|
+
proof: sub,
|
|
389
|
+
publicKey: flags.key ? String(flags.key) : null,
|
|
390
|
+
cwd,
|
|
391
|
+
stateDir: stateDirFor(flags, cwd),
|
|
392
|
+
json: Boolean(flags.json),
|
|
393
|
+
});
|
|
394
|
+
if (output) process.stdout.write(output + "\n");
|
|
395
|
+
// Exit 1 on a failed verification so CI can gate on it. A verifier that
|
|
396
|
+
// always exits 0 is a verifier nobody can automate.
|
|
397
|
+
if (exitCode) process.exitCode = exitCode;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
288
401
|
case "scan": {
|
|
289
402
|
const { result, output } = await scan({
|
|
290
403
|
cwd,
|
|
@@ -414,11 +527,32 @@ async function main() {
|
|
|
414
527
|
});
|
|
415
528
|
process.stdin.on("data", (c) => framer.push(c));
|
|
416
529
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
530
|
+
// Premium gateway startup — to stderr so stdout stays JSON-RPC clean.
|
|
531
|
+
{
|
|
532
|
+
const animated = shouldAnimate({ pace: flags.pace ? Number(flags.pace) : 700, json: false });
|
|
533
|
+
const gwRules = daemon?.currentRules().length || rules.length;
|
|
534
|
+
if (animated) {
|
|
535
|
+
try {
|
|
536
|
+
process.stderr.write("\n" + brandHeader({ width: 62 }) + "\n\n");
|
|
537
|
+
} catch {}
|
|
538
|
+
}
|
|
539
|
+
log(`gateway up · ${Object.keys(servers).length} upstream · ${gwRules} rules` + (daemon ? ` · synced with ${apiUrl}` : " · local policy"));
|
|
540
|
+
// Also emit a small protected panel for human visibility (stderr).
|
|
541
|
+
try {
|
|
542
|
+
const gwPanel = panel({
|
|
543
|
+
lines: [
|
|
544
|
+
`${bold("CIRVIX GATEWAY")}`,
|
|
545
|
+
``,
|
|
546
|
+
`${"Upstreams".padEnd(12)} ${Object.keys(servers).length}`,
|
|
547
|
+
`${"Policy".padEnd(12)} ${green(bold("● ENFORCING"))} ${dim(plural(gwRules, "rule"))}`,
|
|
548
|
+
`${"Audit".padEnd(12)} ${green(bold("● RECORDING"))}`,
|
|
549
|
+
`${"Mode".padEnd(12)} ${dim(daemon ? "synced" : "local")}`,
|
|
550
|
+
],
|
|
551
|
+
width: 48,
|
|
552
|
+
});
|
|
553
|
+
process.stderr.write(gwPanel + "\n");
|
|
554
|
+
} catch {}
|
|
555
|
+
}
|
|
422
556
|
|
|
423
557
|
await new Promise((resolve) => {
|
|
424
558
|
let closing = false;
|
|
@@ -527,6 +661,9 @@ async function main() {
|
|
|
527
661
|
` ${dim("rule")} ${decision.rule ?? dim("— no rule matched (default deny)")}`,
|
|
528
662
|
` ${dim("reason")} ${decision.reason}`,
|
|
529
663
|
decision.remediation ? ` ${dim("fix")} ${blue(decision.remediation)}` : "",
|
|
664
|
+
decision.verdict === "deny" && !flags.json
|
|
665
|
+
? ` ${dim("share")} ${dim(`was this a good catch? ${SHARE_URL} — redact first, your call`)}`
|
|
666
|
+
: "",
|
|
530
667
|
decision.approvers?.length
|
|
531
668
|
? ` ${dim("waits")} ${decision.approvers.join(", ")}`
|
|
532
669
|
: "",
|
|
@@ -559,25 +696,75 @@ async function main() {
|
|
|
559
696
|
return d.verdict === "deny" ? 1 : 0;
|
|
560
697
|
}
|
|
561
698
|
|
|
562
|
-
const
|
|
699
|
+
const isWhyDeny = d.verdict === "deny";
|
|
700
|
+
const isWhyHold = d.verdict === "hold";
|
|
701
|
+
const whyTone = isWhyDeny ? red : isWhyHold ? amber : green;
|
|
702
|
+
const whyDecision = isWhyDeny ? "✕ BLOCKED" : isWhyHold ? "● AWAITING APPROVAL" : "✓ " + String(d.verdict).toUpperCase();
|
|
703
|
+
const whyRisk = String(d.risk ?? "unknown").toUpperCase();
|
|
704
|
+
const whyRiskTone = whyRisk === "CRITICAL" ? red : whyRisk === "HIGH" ? amber : whyRisk === "MEDIUM" ? blue : dim;
|
|
563
705
|
process.stdout.write(
|
|
564
706
|
[
|
|
565
707
|
"",
|
|
566
|
-
` ${
|
|
567
|
-
|
|
568
|
-
` ${dim("
|
|
569
|
-
` ${dim("
|
|
570
|
-
` ${dim("when")} ${d.ts}`,
|
|
571
|
-
// The whole point of this command in an incident: it hands you the
|
|
572
|
-
// thread to pull, not just the one bead you arrived holding.
|
|
573
|
-
` ${dim("run")} ${d.runId ? blue(d.runId) : dim("— recorded outside a run")}`,
|
|
708
|
+
` ${bold("CIRVIX DECISION ANALYSIS")}`,
|
|
709
|
+
"",
|
|
710
|
+
` ${dim("Decision".padEnd(12))} ${whyTone(bold(whyDecision))}`,
|
|
711
|
+
` ${dim("Risk".padEnd(12))} ${whyRiskTone(bold(whyRisk))}`,
|
|
574
712
|
"",
|
|
713
|
+
` ${dim("Tool".padEnd(12))} ${bold(String(d.tool ?? d.action ?? "—"))}`,
|
|
714
|
+
d.resource ? ` ${dim("Target".padEnd(12))} ${d.resource}` : "",
|
|
715
|
+
d.destination ? ` ${dim("Destination".padEnd(12))} ${d.destination}` : "",
|
|
716
|
+
` ${dim("Matched policy".padEnd(12))} ${d.rule ?? dim("— no rule matched (default deny)")}`,
|
|
717
|
+
d.reason ? ` ${dim("Reason".padEnd(12))} ${d.reason}` : "",
|
|
718
|
+
` ${dim("Agent".padEnd(12))} ${d.agent ?? dim("—")}`,
|
|
719
|
+
` ${dim("When".padEnd(12))} ${d.ts}`,
|
|
720
|
+
` ${dim("Run".padEnd(12))} ${d.runId ? blue(d.runId) : dim("— recorded outside a run")}`,
|
|
721
|
+
"",
|
|
722
|
+
` ${dim("Decision path")}`,
|
|
723
|
+
` ${dim("secret detection")}`,
|
|
724
|
+
` ${dim("↓")}`,
|
|
725
|
+
` ${dim("risk classification")}`,
|
|
726
|
+
` ${dim("↓")}`,
|
|
727
|
+
` ${dim("policy evaluation")}`,
|
|
728
|
+
` ${dim("↓")}`,
|
|
729
|
+
` ${whyTone(isWhyDeny ? "BLOCK" : isWhyHold ? "HOLD" : "ALLOW")}`,
|
|
730
|
+
"",
|
|
731
|
+
/* The trifecta is the one refusal whose reason lives outside this
|
|
732
|
+
call, so it gets the sequence rendered rather than a rule name.
|
|
733
|
+
"Blocked: trifecta" is indistinguishable from a bug; three
|
|
734
|
+
timestamped steps are something the reader can act on. */
|
|
735
|
+
...(d.trifecta?.complete
|
|
736
|
+
? [
|
|
737
|
+
` ${whyTone(bold("LETHAL TRIFECTA"))} ${dim("all three conditions met in this session")}`,
|
|
738
|
+
"",
|
|
739
|
+
...["sensitive_data", "untrusted_content", "outbound_action"]
|
|
740
|
+
.map((k) => [k, d.trifecta.legs?.[k]])
|
|
741
|
+
.filter(([, v]) => v)
|
|
742
|
+
.sort((a, b) => String(a[1].at ?? "").localeCompare(String(b[1].at ?? "")))
|
|
743
|
+
.map(
|
|
744
|
+
([k, v], i) =>
|
|
745
|
+
` ${bold(String(i + 1) + ".")} ${dim(k.replace(/_/g, " ").padEnd(18))} ${v.why}` +
|
|
746
|
+
(v.at ? `
|
|
747
|
+
${dim(v.at)}` : ""),
|
|
748
|
+
),
|
|
749
|
+
"",
|
|
750
|
+
` ${dim("Cirvix refuses on capability and opportunity. It does not claim the")}`,
|
|
751
|
+
` ${dim("sensitive bytes are in this request — that needs data-flow analysis")}`,
|
|
752
|
+
` ${dim("it deliberately does not do.")}`,
|
|
753
|
+
"",
|
|
754
|
+
]
|
|
755
|
+
: d.trifecta?.satisfied?.length
|
|
756
|
+
? [
|
|
757
|
+
` ${dim("trifecta")} ${d.trifecta.satisfied.length} of 3 conditions met` +
|
|
758
|
+
(d.trifecta.imminent ? ` ${whyRiskTone("one step from complete")}` : ""),
|
|
759
|
+
"",
|
|
760
|
+
]
|
|
761
|
+
: []),
|
|
575
762
|
...(d.considered?.length
|
|
576
763
|
? [
|
|
577
|
-
` ${dim("considered")}`,
|
|
764
|
+
` ${dim("considered")} ${dim(`${d.considered.filter((c) => c.matched).length} of ${d.considered.length} matched`)}`,
|
|
578
765
|
...d.considered.map(
|
|
579
766
|
(c) =>
|
|
580
|
-
` ${c.matched ? bold("→") : dim(" ")} ${dim(String(c.effect).padEnd(
|
|
767
|
+
` ${c.matched ? bold("→") : dim(" ")} ${dim(String(c.effect).padEnd(11))} ${c.matched ? c.rule : dim(c.rule)}`,
|
|
581
768
|
),
|
|
582
769
|
"",
|
|
583
770
|
]
|
|
@@ -659,11 +846,43 @@ async function main() {
|
|
|
659
846
|
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
660
847
|
return res.ok ? 0 : 1;
|
|
661
848
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
849
|
+
if (res.ok) {
|
|
850
|
+
const W = 62;
|
|
851
|
+
const top = ` ${dim(`╭─ CIRVIX AUDIT VERIFICATION ${"─".repeat(Math.max(0, W - 26))}╮`)}`;
|
|
852
|
+
const bottom = ` ${dim(`╰${"─".repeat(W)}╯`)}`;
|
|
853
|
+
const chainLines = [
|
|
854
|
+
``,
|
|
855
|
+
` ${green("✓")} ${dim("Hash chain intact")}`,
|
|
856
|
+
` ${green("✓")} ${dim(`${res.records} records verified`)}`,
|
|
857
|
+
` ${green("✓")} ${dim("No records altered")}`,
|
|
858
|
+
``,
|
|
859
|
+
` ${bold("CHAIN")}`,
|
|
860
|
+
``,
|
|
861
|
+
` ${dim("current")}`,
|
|
862
|
+
` ${dim("↓")}`,
|
|
863
|
+
` ${dim("previous")}`,
|
|
864
|
+
` ${dim("↓")}`,
|
|
865
|
+
` ${dim("previous")}`,
|
|
866
|
+
` ${dim("↓")}`,
|
|
867
|
+
` ${dim("genesis")}`,
|
|
868
|
+
``,
|
|
869
|
+
` ${bold("STATUS")} ${green(bold("● INTEGRITY OK"))}`,
|
|
870
|
+
``,
|
|
871
|
+
` ${dim(`head ${String(res.head ?? "").slice(0, 16)}…`)}`,
|
|
872
|
+
``,
|
|
873
|
+
` ${dim("Verification proves records were not altered after they were written.")}`,
|
|
874
|
+
` ${dim("It does not attest to their content.")}`,
|
|
875
|
+
``,
|
|
876
|
+
];
|
|
877
|
+
process.stdout.write(`\n${top}\n`);
|
|
878
|
+
process.stdout.write(`\n ${bold("CIRVIX AUDIT VERIFICATION")}\n`);
|
|
879
|
+
for (const l of chainLines) process.stdout.write(l + "\n");
|
|
880
|
+
process.stdout.write(`${bottom}\n\n`);
|
|
881
|
+
} else {
|
|
882
|
+
process.stdout.write(
|
|
883
|
+
`\n ${red(bold("chain broken"))} ${dim(`at record ${res.brokenAt} of ${res.records}`)}\n ${res.reason}\n\n`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
667
886
|
return res.ok ? 0 : 1;
|
|
668
887
|
}
|
|
669
888
|
|
|
@@ -726,6 +945,22 @@ async function main() {
|
|
|
726
945
|
return code;
|
|
727
946
|
}
|
|
728
947
|
|
|
948
|
+
case "simulate": {
|
|
949
|
+
const { simulatePolicy } = await import("../src/commands/simulate.mjs");
|
|
950
|
+
const { output, code } = await simulatePolicy({
|
|
951
|
+
rules: loaded.rules,
|
|
952
|
+
action: flags.action ?? flags.tool ?? "fs:read",
|
|
953
|
+
resource: flags.resource ?? flags.path ?? flags.command ?? "",
|
|
954
|
+
tool: flags.tool ?? "file_reader",
|
|
955
|
+
intent: flags.intent ?? null,
|
|
956
|
+
agent: String(flags.agent ?? "local"),
|
|
957
|
+
json: Boolean(flags.json),
|
|
958
|
+
cwd,
|
|
959
|
+
});
|
|
960
|
+
process.stdout.write(output + "\n");
|
|
961
|
+
return code;
|
|
962
|
+
}
|
|
963
|
+
|
|
729
964
|
case "list":
|
|
730
965
|
default: {
|
|
731
966
|
const { output, code } = policyCmd.list(loaded.rules, {
|
|
@@ -745,11 +980,76 @@ async function main() {
|
|
|
745
980
|
cwd,
|
|
746
981
|
json: Boolean(flags.json),
|
|
747
982
|
force: Boolean(flags.force),
|
|
983
|
+
apply: Boolean(flags.apply),
|
|
984
|
+
dryRun: Boolean(flags["dry-run"]),
|
|
985
|
+
rollback: flags.rollback ? (typeof flags.rollback === "string" ? flags.rollback : true) : false,
|
|
748
986
|
});
|
|
749
987
|
process.stdout.write(output + "\n");
|
|
750
988
|
return result.ok ? 0 : 1;
|
|
751
989
|
}
|
|
752
990
|
|
|
991
|
+
/* ------------------------------------------------------------ simulate */
|
|
992
|
+
case "simulate": {
|
|
993
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
994
|
+
const { simulatePolicy } = await import("../src/commands/simulate.mjs");
|
|
995
|
+
const { output, code } = await simulatePolicy({
|
|
996
|
+
rules,
|
|
997
|
+
action: flags.action ?? flags.tool ?? positional[1] ?? "fs:read",
|
|
998
|
+
resource: flags.resource ?? flags.path ?? flags.command ?? positional[2] ?? "",
|
|
999
|
+
tool: flags.tool ?? "file_reader",
|
|
1000
|
+
intent: flags.intent ?? null,
|
|
1001
|
+
agent: String(flags.agent ?? "local"),
|
|
1002
|
+
json: Boolean(flags.json),
|
|
1003
|
+
cwd,
|
|
1004
|
+
});
|
|
1005
|
+
process.stdout.write(output + "\n");
|
|
1006
|
+
return code;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/* ----------------------------------------------------------------- kill */
|
|
1010
|
+
case "kill": {
|
|
1011
|
+
const { executeKillCommand } = await import("../src/commands/kill.mjs");
|
|
1012
|
+
const { output, code } = await executeKillCommand({
|
|
1013
|
+
scope: flags.scope ?? "agent",
|
|
1014
|
+
target: positional[1] ?? flags.target ?? null,
|
|
1015
|
+
reason: flags.reason ?? "Emergency freeze triggered via CLI",
|
|
1016
|
+
release: flags.release ?? null,
|
|
1017
|
+
list: Boolean(flags.list),
|
|
1018
|
+
json: Boolean(flags.json),
|
|
1019
|
+
});
|
|
1020
|
+
process.stdout.write(output + "\n");
|
|
1021
|
+
return code;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/* --------------------------------------------------------------- shadow */
|
|
1025
|
+
case "shadow": {
|
|
1026
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
1027
|
+
const { executeShadowCommand } = await import("../src/commands/shadow.mjs");
|
|
1028
|
+
const { output, code } = await executeShadowCommand({
|
|
1029
|
+
rules,
|
|
1030
|
+
action: flags.action ?? positional[1] ?? null,
|
|
1031
|
+
resource: flags.resource ?? positional[2] ?? null,
|
|
1032
|
+
json: Boolean(flags.json),
|
|
1033
|
+
cwd,
|
|
1034
|
+
});
|
|
1035
|
+
process.stdout.write(output + "\n");
|
|
1036
|
+
return code;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/* -------------------------------------------------------------- redteam */
|
|
1040
|
+
case "redteam": {
|
|
1041
|
+
const rules = await loadRules(flags.policy, cwd);
|
|
1042
|
+
const { executeRedTeamCommand } = await import("../src/commands/redteam.mjs");
|
|
1043
|
+
const { output, code } = await executeRedTeamCommand({
|
|
1044
|
+
rules,
|
|
1045
|
+
plugins: flags.plugins ? String(flags.plugins).split(",") : null,
|
|
1046
|
+
json: Boolean(flags.json),
|
|
1047
|
+
cwd,
|
|
1048
|
+
});
|
|
1049
|
+
process.stdout.write(output + "\n");
|
|
1050
|
+
return code;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
753
1053
|
/* -------------------------------------------------------------- status */
|
|
754
1054
|
case "upgrade": {
|
|
755
1055
|
// `positional` already has the command at [0]; the rest are the
|
|
@@ -791,6 +1091,66 @@ async function main() {
|
|
|
791
1091
|
case "logs": {
|
|
792
1092
|
const stateDir = stateDirFor(flags, cwd);
|
|
793
1093
|
const file = String(flags.file ?? join(stateDir, "audit.jsonl"));
|
|
1094
|
+
|
|
1095
|
+
// Live mode: cirvix logs --watch
|
|
1096
|
+
if (flags.watch || flags.follow || flags.w) {
|
|
1097
|
+
if (flags.json) {
|
|
1098
|
+
process.stderr.write(red(" --watch is not compatible with --json.\n"));
|
|
1099
|
+
return 2;
|
|
1100
|
+
}
|
|
1101
|
+
const { watch } = await import("node:fs");
|
|
1102
|
+
const live = new LiveStream({ stream: process.stdout, title: "CIRVIX LIVE · protection active" });
|
|
1103
|
+
// Print existing tail first
|
|
1104
|
+
const existing = await journal.read(file);
|
|
1105
|
+
const tail = journal.query(existing, {
|
|
1106
|
+
last: flags.last ? Number(flags.last) : 10,
|
|
1107
|
+
risk: typeof flags.risk === "string" ? flags.risk : undefined,
|
|
1108
|
+
decision: typeof flags.decision === "string" ? flags.decision : undefined,
|
|
1109
|
+
});
|
|
1110
|
+
live.header();
|
|
1111
|
+
for (const r of tail) live.push(r);
|
|
1112
|
+
if (tail.length === 0) {
|
|
1113
|
+
process.stdout.write(` ${dim("waiting for decisions…")} ${dim(`tailing ${file}`)}\n`);
|
|
1114
|
+
}
|
|
1115
|
+
// Watch for new records — polling via fs.watch where available, fallback to interval.
|
|
1116
|
+
let known = existing.length;
|
|
1117
|
+
let watcher = null;
|
|
1118
|
+
let polling = null;
|
|
1119
|
+
const emitNew = async () => {
|
|
1120
|
+
const all = await journal.read(file);
|
|
1121
|
+
if (all.length > known) {
|
|
1122
|
+
const fresh = all.slice(known);
|
|
1123
|
+
const filtered = journal.query(fresh, {
|
|
1124
|
+
risk: typeof flags.risk === "string" ? flags.risk : undefined,
|
|
1125
|
+
decision: typeof flags.decision === "string" ? flags.decision : undefined,
|
|
1126
|
+
agent: typeof flags.agent === "string" ? flags.agent : undefined,
|
|
1127
|
+
tool: typeof flags.tool === "string" ? flags.tool : undefined,
|
|
1128
|
+
deniedOnly: Boolean(flags.denied),
|
|
1129
|
+
});
|
|
1130
|
+
for (const r of filtered) live.push(r);
|
|
1131
|
+
known = all.length;
|
|
1132
|
+
} else if (all.length < known) {
|
|
1133
|
+
known = all.length;
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
try {
|
|
1137
|
+
watcher = watch(file, async () => { await emitNew().catch(() => {}); });
|
|
1138
|
+
} catch {
|
|
1139
|
+
polling = setInterval(() => void emitNew(), 700);
|
|
1140
|
+
}
|
|
1141
|
+
if (!watcher) polling = setInterval(() => void emitNew(), 700);
|
|
1142
|
+
await new Promise((resolve) => {
|
|
1143
|
+
const done = () => {
|
|
1144
|
+
try { watcher?.close(); } catch {}
|
|
1145
|
+
if (polling) clearInterval(polling);
|
|
1146
|
+
resolve();
|
|
1147
|
+
};
|
|
1148
|
+
process.on("SIGINT", done);
|
|
1149
|
+
process.on("SIGTERM", done);
|
|
1150
|
+
});
|
|
1151
|
+
return 0;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
794
1154
|
const records = await journal.read(file);
|
|
795
1155
|
|
|
796
1156
|
// `--tree <id>` prints one decision in full rather than the list.
|
|
@@ -870,20 +1230,29 @@ async function main() {
|
|
|
870
1230
|
process.stdout.write("\n " + bold(plural(pending.length, "call")) + dim(" waiting\n\n"));
|
|
871
1231
|
for (const a of pending) {
|
|
872
1232
|
const riskTone = { low: dim, medium: blue, high: amber, critical: red }[a.risk] ?? dim;
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
)
|
|
876
|
-
process.stdout.write(
|
|
877
|
-
process.stdout.write(
|
|
878
|
-
|
|
879
|
-
);
|
|
1233
|
+
const W = 62;
|
|
1234
|
+
const top = ` ${dim(`╭─ HUMAN APPROVAL REQUIRED ${"─".repeat(Math.max(0, W - 28))}╮`)}`;
|
|
1235
|
+
const bottom = ` ${dim(`╰${"─".repeat(W)}╯`)}`;
|
|
1236
|
+
process.stdout.write(top + "\n");
|
|
1237
|
+
process.stdout.write(` ${dim("│")} ${dim("Agent".padEnd(10))} ${a.agent ?? "—"} ${dim("│")}\n`);
|
|
1238
|
+
process.stdout.write(` ${dim("│")} ${dim("Action".padEnd(10))} ${a.tool ?? "—"} ${dim("│")}\n`);
|
|
1239
|
+
process.stdout.write(` ${dim("│")} ${dim("Target".padEnd(10))} ${String(a.resource ?? "").slice(0, 32).padEnd(32)} ${dim("│")}\n`);
|
|
1240
|
+
process.stdout.write(` ${dim("│")} ${"".padEnd(46)} ${dim("│")}\n`);
|
|
1241
|
+
process.stdout.write(` ${dim("│")} ${dim("Risk".padEnd(10))} ${riskTone(String(a.risk ?? "").toUpperCase().padEnd(9))} ${dim("│")}\n`);
|
|
1242
|
+
process.stdout.write(` ${dim("│")} ${dim("Policy".padEnd(10))} ${String(a.rule ?? "—").slice(0, 32).padEnd(32)} ${dim("│")}\n`);
|
|
1243
|
+
process.stdout.write(` ${dim("│")} ${dim("Waits on".padEnd(10))} ${(a.approvers ?? []).join(", ") || "—"} ${dim("│")}\n`);
|
|
1244
|
+
process.stdout.write(` ${dim("│")} ${"".padEnd(46)} ${dim("│")}\n`);
|
|
1245
|
+
process.stdout.write(` ${dim("│")} ${dim("Reason")} ${dim("│")}\n`);
|
|
1246
|
+
process.stdout.write(` ${dim("│")} ${(a.reason ?? "Production database mutation requires human authorization.").slice(0, 44).padEnd(44)} ${dim("│")}\n`);
|
|
1247
|
+
process.stdout.write(` ${dim("│")} ${"".padEnd(46)} ${dim("│")}\n`);
|
|
1248
|
+
process.stdout.write(` ${dim("│")} ${green("[A] Approve")} ${dim(" ")} ${red("[R] Reject")} ${dim(` ${a.id}`)} ${dim("│")}\n`);
|
|
1249
|
+
process.stdout.write(bottom + "\n\n");
|
|
880
1250
|
if (a.state !== "pending") {
|
|
881
|
-
process.stdout.write(` ${dim("state")} ${a.state}${a.decidedBy ? dim(` by ${a.decidedBy}`) : ""}\n`);
|
|
1251
|
+
process.stdout.write(` ${dim("state")} ${a.state}${a.decidedBy ? dim(` by ${a.decidedBy}`) : ""}\n\n`);
|
|
882
1252
|
}
|
|
883
|
-
process.stdout.write("\n");
|
|
884
1253
|
}
|
|
885
1254
|
process.stdout.write(
|
|
886
|
-
` ${dim("Decide one:")} ${blue(`cirvix approve ${pending[0].id} --by you@example.com`)}\n\n`,
|
|
1255
|
+
` ${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`,
|
|
887
1256
|
);
|
|
888
1257
|
return 0;
|
|
889
1258
|
}
|
|
@@ -974,6 +1343,10 @@ async function main() {
|
|
|
974
1343
|
meter: runtimeMeter,
|
|
975
1344
|
write: (s) => process.stderr.write(s),
|
|
976
1345
|
});
|
|
1346
|
+
/* Measured, not asserted. The panel below reports these, and the only
|
|
1347
|
+
honest source for them is the decision stream itself. */
|
|
1348
|
+
const counters = { blocked: 0, approvals: 0, violations: 0 };
|
|
1349
|
+
const seenAgents = new Set();
|
|
977
1350
|
const pipeline = new Pipeline({
|
|
978
1351
|
rules,
|
|
979
1352
|
cwd,
|
|
@@ -987,7 +1360,14 @@ async function main() {
|
|
|
987
1360
|
meter: runtimeMeter,
|
|
988
1361
|
agents: new AgentRegistry(),
|
|
989
1362
|
onEvent: (e) => {
|
|
990
|
-
if (e.kind === "decision")
|
|
1363
|
+
if (e.kind === "decision") {
|
|
1364
|
+
notice(e);
|
|
1365
|
+
const ev = e.event ?? e;
|
|
1366
|
+
if (ev.agent) seenAgents.add(ev.agent);
|
|
1367
|
+
if (ev.decision === "deny") counters.blocked += 1;
|
|
1368
|
+
else if (ev.decision === "require_approval") counters.approvals += 1;
|
|
1369
|
+
if (ev.risk === "critical") counters.violations += 1;
|
|
1370
|
+
}
|
|
991
1371
|
},
|
|
992
1372
|
log: (m) => process.stderr.write(`[cirvix] ${m}\n`),
|
|
993
1373
|
});
|
|
@@ -1013,14 +1393,70 @@ async function main() {
|
|
|
1013
1393
|
});
|
|
1014
1394
|
await server.start();
|
|
1015
1395
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1396
|
+
// Premium startup sequence — brand + real state, zero fake.
|
|
1397
|
+
const runtimeAnimated = shouldAnimate({ pace: flags.pace ? Number(flags.pace) : 700, json: Boolean(flags.json) });
|
|
1398
|
+
// Compute policy tests count for display (real).
|
|
1399
|
+
let rtTests = 0;
|
|
1400
|
+
try {
|
|
1401
|
+
const { loadPolicyFile } = await import("../src/commands/policy.mjs");
|
|
1402
|
+
let policyPath = null;
|
|
1403
|
+
for (const cand of ["cirvix.policy", "cirvix.policy.json", ".cirvix/policy.json"]) {
|
|
1404
|
+
const p = join(cwd, cand);
|
|
1405
|
+
try { await access(p); policyPath = p; break; } catch {}
|
|
1406
|
+
}
|
|
1407
|
+
if (policyPath) {
|
|
1408
|
+
const loaded = await loadPolicyFile(policyPath, { cwd });
|
|
1409
|
+
rtTests = loaded.tests?.length ?? 0;
|
|
1410
|
+
}
|
|
1411
|
+
} catch {}
|
|
1412
|
+
if (!flags.json) {
|
|
1413
|
+
// Brand header only when interactive; in CI/non-TTY just show compact.
|
|
1414
|
+
if (runtimeAnimated) {
|
|
1415
|
+
process.stdout.write("\n" + brandHeader({ width: 62 }) + "\n\n");
|
|
1416
|
+
} else {
|
|
1417
|
+
process.stdout.write(`\n ${bold("CIRVIX")} ${dim("· runtime governance")}\n\n`);
|
|
1418
|
+
}
|
|
1419
|
+
process.stdout.write(` ${dim("Initializing CIRVIX runtime...")}\n`);
|
|
1420
|
+
process.stdout.write(` ${green("✓")} ${dim("Control socket established")} ${dim(endpoint)}\n`);
|
|
1421
|
+
process.stdout.write(` ${green("✓")} ${dim("Policy engine loaded")}\n`);
|
|
1422
|
+
process.stdout.write(` ${green("✓")} ${dim("Secret protection enabled")}\n`);
|
|
1423
|
+
process.stdout.write(` ${green("✓")} ${dim("Audit chain initialized")}\n`);
|
|
1424
|
+
process.stdout.write(` ${green("✓")} ${dim(`${plural(rules.length, "rule")} loaded`)}${rtTests ? dim(` · ${rtTests} policy tests`) : ""}\n`);
|
|
1425
|
+
process.stdout.write("\n");
|
|
1426
|
+
// The agent this runtime was started for, plus any that have since
|
|
1427
|
+
// announced themselves. One at startup is a fact, not a placeholder —
|
|
1428
|
+
// but only because it is counted.
|
|
1429
|
+
const agentsSeen = Math.max(seenAgents.size, 1);
|
|
1430
|
+
const protectedLines = [
|
|
1431
|
+
`${bold("CIRVIX PROTECTED")}`,
|
|
1432
|
+
``,
|
|
1433
|
+
`${"Runtime".padEnd(12)} ${green(bold("● ONLINE"))} ${dim(mode === MODE.AUDIT ? "AUDIT · recording only" : "ENFORCING")}`,
|
|
1434
|
+
`${"Policy".padEnd(12)} ${green(bold("● ENFORCING"))} ${dim(plural(rules.length, "rule"))}`,
|
|
1435
|
+
`${"Secrets".padEnd(12)} ${green(bold("● PROTECTED"))}`,
|
|
1436
|
+
`${"Audit".padEnd(12)} ${green(bold("● RECORDING"))}`,
|
|
1437
|
+
`${"Agents".padEnd(12)} ${dim(plural(agentsSeen, "detected", "detected"))}`,
|
|
1438
|
+
``,
|
|
1439
|
+
/* These were string literals — `1 detected`, `0 blocked · 0
|
|
1440
|
+
approvals · 0 violations`. They happened to be true at startup,
|
|
1441
|
+
which is exactly what makes that kind of line dangerous: it reads
|
|
1442
|
+
as measurement, it survives review, and it is wrong the moment
|
|
1443
|
+
anything happens. Counted from the pipeline now. */
|
|
1444
|
+
`${dim(`${counters.blocked} blocked · ${counters.approvals} approvals · ${counters.violations} violations`)}`,
|
|
1445
|
+
];
|
|
1446
|
+
process.stdout.write(panel({ lines: protectedLines, width: 62 }) + "\n\n");
|
|
1447
|
+
process.stdout.write(` ${dim("Ready. Your agent is under policy control.")}\n`);
|
|
1448
|
+
process.stdout.write(` ${dim(`token in ${join(stateDir, "socket.token")}`)}\n\n`);
|
|
1449
|
+
if (mode === MODE.AUDIT) {
|
|
1450
|
+
process.stdout.write(` ${amber(bold("AUDIT MODE"))} ${dim("— decisions are recorded and nothing is blocked.")}\n\n`);
|
|
1451
|
+
}
|
|
1452
|
+
} else {
|
|
1021
1453
|
process.stdout.write(
|
|
1022
|
-
|
|
1454
|
+
`\n ${green(bold("runtime up"))} ${dim(`${plural(rules.length, "rule")} · ${mode} · ${endpoint}`)}\n` +
|
|
1455
|
+
` ${dim(`token in ${join(stateDir, "socket.token")}`)}\n\n`,
|
|
1023
1456
|
);
|
|
1457
|
+
if (mode === MODE.AUDIT) {
|
|
1458
|
+
process.stdout.write(` ${amber(bold("AUDIT MODE"))} ${dim("— decisions are recorded and nothing is blocked.")}\n\n`);
|
|
1459
|
+
}
|
|
1024
1460
|
}
|
|
1025
1461
|
|
|
1026
1462
|
await new Promise((resolve) => {
|
|
@@ -1038,6 +1474,18 @@ async function main() {
|
|
|
1038
1474
|
return 0;
|
|
1039
1475
|
}
|
|
1040
1476
|
|
|
1477
|
+
case "doctor": {
|
|
1478
|
+
return doctor({ cwd, json: Boolean(flags.json) });
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
case "login": {
|
|
1482
|
+
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) });
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
case "logout": {
|
|
1486
|
+
return logout({ json: Boolean(flags.json) });
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1041
1489
|
case "help":
|
|
1042
1490
|
default:
|
|
1043
1491
|
process.stdout.write(HELP + "\n");
|