@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.
Files changed (70) hide show
  1. package/README.md +76 -17
  2. package/bin/cirvix.mjs +488 -40
  3. package/bin/escape-benchmark.mjs +67 -0
  4. package/package.json +36 -16
  5. package/src/adapters/base.mjs +150 -0
  6. package/src/adapters/claude-code.mjs +161 -0
  7. package/src/adapters/cline.mjs +107 -0
  8. package/src/adapters/codex.mjs +104 -0
  9. package/src/adapters/cursor.mjs +104 -0
  10. package/src/adapters/frameworks.mjs +110 -0
  11. package/src/adapters/gemini-cli.mjs +104 -0
  12. package/src/adapters/generic-mcp.mjs +101 -0
  13. package/src/adapters/index.mjs +209 -0
  14. package/src/adapters/roo-code.mjs +106 -0
  15. package/src/adapters/vscode.mjs +104 -0
  16. package/src/adapters/windsurf.mjs +107 -0
  17. package/src/commands/demo.mjs +56 -70
  18. package/src/commands/doctor.mjs +235 -0
  19. package/src/commands/init.mjs +292 -30
  20. package/src/commands/interactive.mjs +690 -0
  21. package/src/commands/kill.mjs +74 -0
  22. package/src/commands/login.mjs +227 -0
  23. package/src/commands/passport.mjs +149 -0
  24. package/src/commands/policy.mjs +10 -6
  25. package/src/commands/protect.mjs +293 -0
  26. package/src/commands/prove.mjs +209 -0
  27. package/src/commands/redteam.mjs +51 -0
  28. package/src/commands/scan.mjs +6 -4
  29. package/src/commands/shadow.mjs +62 -0
  30. package/src/commands/simulate.mjs +96 -0
  31. package/src/commands/status.mjs +121 -36
  32. package/src/commands/upgrade.mjs +17 -9
  33. package/src/commands/welcome.mjs +105 -0
  34. package/src/core/authority.mjs +909 -0
  35. package/src/core/baseline.mjs +97 -0
  36. package/src/core/config-store.mjs +280 -0
  37. package/src/core/cost.mjs +0 -0
  38. package/src/core/detect.mjs +4 -33
  39. package/src/core/entitlements.mjs +6 -0
  40. package/src/core/escape-benchmark.mjs +597 -0
  41. package/src/core/evidence.mjs +212 -0
  42. package/src/core/format.mjs +27 -0
  43. package/src/core/gateway.mjs +15 -211
  44. package/src/core/graph.mjs +270 -0
  45. package/src/core/guard.mjs +118 -4
  46. package/src/core/intent.mjs +166 -0
  47. package/src/core/journal.mjs +131 -40
  48. package/src/core/kill-switch.mjs +122 -0
  49. package/src/core/notices.mjs +22 -2
  50. package/src/core/packs.mjs +193 -0
  51. package/src/core/passport.mjs +555 -0
  52. package/src/core/pipeline.mjs +148 -6
  53. package/src/core/prompts.mjs +51 -0
  54. package/src/core/proof.mjs +440 -0
  55. package/src/core/redteam/index.mjs +185 -0
  56. package/src/core/referral.mjs +187 -0
  57. package/src/core/sandbox.mjs +139 -0
  58. package/src/core/session.mjs +172 -0
  59. package/src/core/shadow.mjs +95 -0
  60. package/src/core/trifecta.mjs +321 -0
  61. package/src/core/ui/controller.mjs +192 -0
  62. package/src/core/ui/decisions.mjs +55 -0
  63. package/src/core/ui/index.mjs +49 -0
  64. package/src/core/ui/intercept.mjs +103 -0
  65. package/src/core/ui/live.mjs +51 -0
  66. package/src/core/ui/primitives.mjs +123 -0
  67. package/src/core/ui/theme.mjs +92 -0
  68. package/src/core/verified.mjs +108 -0
  69. package/src/core/windows.mjs +270 -0
  70. package/src/index.mjs +25 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * SecurityIntercept — visually distinctive blocked-action panel.
3
+ *
4
+ * Animation MUST NOT delay enforcement. Decision is already computed.
5
+ * This only visualizes it with a short 300-800ms sequence.
6
+ */
7
+
8
+ import { bold, dim, red, stripAnsi } from "../format.mjs";
9
+ import { shouldAnimate, sleep } from "./controller.mjs";
10
+ import { boxChars, truncate, padVisible } from "./theme.mjs";
11
+
12
+ export function interceptBox(event) {
13
+ const W = 58;
14
+ const ch = boxChars();
15
+ const useAscii = ch.tl === "+";
16
+ // Use red border for intercept.
17
+ const h = useAscii ? ch.h : "═";
18
+ const v = useAscii ? ch.v : "║";
19
+ const tl = useAscii ? ch.tl : "╔";
20
+ const tr = useAscii ? ch.tr : "╗";
21
+ const bl = useAscii ? ch.bl : "╚";
22
+ const br = useAscii ? ch.br : "╝";
23
+ const mj = useAscii ? ch.lt : "╠";
24
+ const mid = mj + h.repeat(W + 2) + (useAscii ? ch.rt : "╣");
25
+
26
+ const rows = [
27
+ ["Agent", event.agent ?? "—"],
28
+ ["Tool", event.tool ?? event.action ?? "—"],
29
+ ["Target", event.resource ?? event.destination ?? "—"],
30
+ ["Risk", String(event.risk ?? "—").toUpperCase()],
31
+ ["Decision", "BLOCKED"],
32
+ ["Policy", event.policy ?? event.rule ?? "default-deny"],
33
+ ["Latency", `${event.latency_ms ?? "—"}ms`],
34
+ ];
35
+
36
+ const pad = (text) => {
37
+ const s = String(text);
38
+ const vis = stripAnsi(s).length;
39
+ if (vis > W) return s.slice(0, W - 1) + "…";
40
+ return s + " ".repeat(W - vis);
41
+ };
42
+
43
+ const lines = [];
44
+ lines.push(` ${red(tl + h.repeat(W + 2) + tr)}`);
45
+ lines.push(` ${red(v)} ${bold(pad("CIRVIX SECURITY INTERCEPT"))} ${red(v)}`);
46
+ lines.push(` ${red(mid)}`);
47
+ for (const [k, val] of rows) {
48
+ const body = `${k}:`.padEnd(11) + String(val);
49
+ const painted = k === "Risk" || k === "Decision" ? red(pad(body)) : pad(body);
50
+ lines.push(` ${red(v)} ${painted} ${red(v)}`);
51
+ }
52
+ lines.push(` ${red(bl + h.repeat(W + 2) + br)}`);
53
+
54
+ const reason = event.reason ? `\n ${dim(truncate(event.reason, 72))}\n` : "";
55
+ return lines.join("\n") + reason;
56
+ }
57
+
58
+ /**
59
+ * Animated intercept: spinner → risk → BLOCKED → policy.
60
+ * Total 300-800ms, but decision already made.
61
+ */
62
+ export async function animateIntercept(event, { stream = process.stdout, pace = 700 } = {}) {
63
+ const enabled = shouldAnimate({ pace, json: false });
64
+ if (!enabled) {
65
+ stream.write(interceptBox(event) + "\n");
66
+ return;
67
+ }
68
+ const write = (s) => {
69
+ try {
70
+ stream.write(s);
71
+ } catch {}
72
+ };
73
+ const timers = [];
74
+ const hide = () => {
75
+ try {
76
+ stream.write("\u001b[?25l");
77
+ } catch {}
78
+ };
79
+ const show = () => {
80
+ try {
81
+ stream.write("\u001b[?25h");
82
+ } catch {}
83
+ };
84
+ hide();
85
+ // Phase 1: evaluating
86
+ write(" ◌ evaluating request...\r");
87
+ await sleep(Math.min(260, pace * 0.35), timers);
88
+ write("\x1b[2K");
89
+ // Phase 2: risk
90
+ const riskTone = event.risk === "critical" ? red : event.risk === "high" ? "\u001b[33m" : "";
91
+ write(` ${riskTone}⚠ ${String(event.risk ?? "").toUpperCase()}\u001b[39m\n`);
92
+ await sleep(Math.min(160, pace * 0.2), timers);
93
+ // Phase 3: BLOCKED
94
+ write(` ${red("✕ BLOCKED")}\n`);
95
+ await sleep(Math.min(160, pace * 0.2), timers);
96
+ // Phase 4: policy
97
+ write(` ${dim(String(event.policy ?? event.rule ?? ""))}\n`);
98
+ await sleep(Math.min(120, pace * 0.15), timers);
99
+ // Phase 5: full box
100
+ write(interceptBox(event) + "\n");
101
+ show();
102
+ for (const t of timers) clearTimeout(t);
103
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * LiveStream — `cirvix logs --watch` live security stream.
3
+ *
4
+ * Line-based updates, no full redraw, readable.
5
+ */
6
+
7
+ import { dim, bold } from "../format.mjs";
8
+ import { renderDecision } from "./decisions.mjs";
9
+
10
+ export class LiveStream {
11
+ constructor({ stream = process.stdout, title = "CIRVIX LIVE · protection active" } = {}) {
12
+ this.stream = stream;
13
+ this.title = title;
14
+ this.started = false;
15
+ }
16
+
17
+ header() {
18
+ if (this.started) return;
19
+ this.started = true;
20
+ try {
21
+ this.stream.write(`\n ${bold(this.title)}\n`);
22
+ this.stream.write(` ${dim("─".repeat(60))}\n\n`);
23
+ } catch {}
24
+ }
25
+
26
+ push(event) {
27
+ this.header();
28
+ const line = renderDecision(event);
29
+ // For live, compact is preferred even for DENY to keep stream readable,
30
+ // but DENY still gets expanded with context on next line.
31
+ try {
32
+ // event already has ts; render with clock if present.
33
+ const clock = event.ts ? String(event.ts).slice(11, 19) : new Date().toISOString().slice(11, 19);
34
+ // If decision renderer already includes multiline, just prefix clock.
35
+ if (line.includes("\n")) {
36
+ const parts = line.split("\n");
37
+ this.stream.write(` ${dim(clock)} ${parts[0].trimStart()}\n`);
38
+ for (let i = 1; i < parts.length; i++) this.stream.write(parts[i] + "\n");
39
+ } else {
40
+ this.stream.write(` ${dim(clock)} ${line.trimStart()}\n`);
41
+ }
42
+ } catch {}
43
+ }
44
+
45
+ footer(stats) {
46
+ if (!stats) return;
47
+ try {
48
+ this.stream.write(`\n ${dim(`${stats.records ?? 0} decisions · P99 ${stats.latency?.p99 ?? 0}ms`)}\n\n`);
49
+ } catch {}
50
+ }
51
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Primitives — Panel, Table, Badge, Logo, Status helpers.
3
+ *
4
+ * Zero deps, unicode with ASCII fallback, respects NO_COLOR via format.mjs.
5
+ */
6
+
7
+ import { bold, dim, stripAnsi, supportsUnicode } from "../format.mjs";
8
+ import { boxChars, truncate, padVisible } from "./theme.mjs";
9
+
10
+ /** CIRVIX ASCII logo (5 lines). Compact, premium, not gamey. */
11
+ export const LOGO_LINES = [
12
+ " ██████╗██╗██████╗ ██╗ ██╗██╗██╗ ██╗",
13
+ "██╔════╝██║██╔══██╗██║ ██║██║╚██╗██╔╝",
14
+ "██║ ██║██████╔╝██║ ██║██║ ╚███╔╝ ",
15
+ "██║ ██║██╔══██╗╚██╗ ██╔╝██║ ██╔██╗ ",
16
+ "╚██████╗██║██║ ██║ ╚████╔╝ ██║██╔╝ ██╗",
17
+ " ╚═════╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚═╝ ╚═╝",
18
+ ];
19
+
20
+ export const LOGO_SUBTITLE = "AI AGENT RUNTIME GOVERNANCE";
21
+
22
+ /**
23
+ * Draw a rounded panel with optional title.
24
+ *
25
+ * @param {object} opts
26
+ * @param {string} [opts.title]
27
+ * @param {string[]} opts.lines — already formatted (may contain ANSI)
28
+ * @param {number} [opts.width] — inner width, default 58
29
+ * @param {boolean} [opts.heavy] — use heavy border for intercept
30
+ */
31
+ export function panel({ title, lines = [], width = 58, heavy = false } = {}) {
32
+ const ch = boxChars();
33
+ const h = heavy ? ch.hHeavy : ch.h;
34
+ const v = heavy ? ch.vHeavy : ch.v;
35
+
36
+ // Compute actual inner width from content if not provided.
37
+ let inner = width;
38
+ if (!width) {
39
+ inner = Math.max(...lines.map((l) => stripAnsi(l).length), title ? stripAnsi(title).length : 0) + 2;
40
+ inner = Math.max(40, Math.min(72, inner));
41
+ }
42
+
43
+ const top = `╭${h.repeat(inner + 2)}╮`;
44
+ const bottom = `╰${h.repeat(inner + 2)}╯`;
45
+ // For ascii fallback, boxChars returns +/-, so above uses unicode literals.
46
+ // Rebuild with actual chars for ascii.
47
+ const useAscii = !supportsUnicode();
48
+ const topLine = useAscii ? `${ch.tl}${ch.h.repeat(inner + 2)}${ch.tr}` : top;
49
+ const bottomLine = useAscii ? `${ch.bl}${ch.h.repeat(inner + 2)}${ch.br}` : bottom;
50
+ const vert = useAscii ? ch.v : v;
51
+
52
+ const out = [];
53
+ out.push(` ${topLine}`);
54
+ if (title) {
55
+ const t = truncate(title, inner);
56
+ out.push(` ${vert} ${padVisible(t, inner)} ${vert}`);
57
+ out.push(` ${vert} ${" ".repeat(inner)} ${vert}`);
58
+ } else {
59
+ out.push(` ${vert} ${" ".repeat(inner)} ${vert}`);
60
+ }
61
+ for (const line of lines) {
62
+ const clean = truncate(stripAnsi(line), inner);
63
+ // Preserve ANSI: pad based on visible length.
64
+ const padded = padVisible(line, inner);
65
+ // Ensure we truncate ANSI correctly — if original had ANSI, we need to re-truncate safely.
66
+ // Simpler: use padded which already uses visibleWidth.
67
+ out.push(` ${vert} ${padded} ${vert}`);
68
+ }
69
+ out.push(` ${vert} ${" ".repeat(inner)} ${vert}`);
70
+ out.push(` ${bottomLine}`);
71
+ return out.join("\n");
72
+ }
73
+
74
+ /**
75
+ * Render the full brand header — logo boxed.
76
+ */
77
+ export function brandHeader({ width = 58 } = {}) {
78
+ const ch = boxChars();
79
+ const useAscii = !supportsUnicode();
80
+ const h = ch.h;
81
+ const top = useAscii ? `${ch.tl}${h.repeat(width + 2)}${ch.tr}` : `╭${h.repeat(width + 2)}╮`;
82
+ const bottom = useAscii ? `${ch.bl}${h.repeat(width + 2)}${ch.br}` : `╰${h.repeat(width + 2)}╯`;
83
+ const vert = useAscii ? ch.v : ch.v;
84
+
85
+ const lines = [];
86
+ lines.push(` ${top}`);
87
+ lines.push(` ${vert} ${" ".repeat(width)} ${vert}`);
88
+ for (const l of LOGO_LINES) {
89
+ const padded = l.padStart(Math.floor((width + l.length) / 2)).padEnd(width);
90
+ lines.push(` ${vert} ${padded} ${vert}`);
91
+ }
92
+ lines.push(` ${vert} ${" ".repeat(width)} ${vert}`);
93
+ const sub = LOGO_SUBTITLE.padStart(Math.floor((width + LOGO_SUBTITLE.length) / 2)).padEnd(width);
94
+ lines.push(` ${vert} ${dim(sub)} ${vert}`);
95
+ lines.push(` ${vert} ${" ".repeat(width)} ${vert}`);
96
+ lines.push(` ${bottom}`);
97
+ return lines.join("\n");
98
+ }
99
+
100
+ /** Simple key/value rows aligned. */
101
+ export function keyValueRows(rows, { keyWidth } = {}) {
102
+ const w = keyWidth ?? Math.max(...rows.map(([k]) => stripAnsi(String(k)).length));
103
+ return rows.map(([k, v]) => ` ${String(k).padEnd(w + 2)}${v}`);
104
+ }
105
+
106
+ /** Separator line. */
107
+ export function separator(width = 60, char = "─") {
108
+ const ch = supportsUnicode() ? char : "-";
109
+ return dim(" " + ch.repeat(width));
110
+ }
111
+
112
+ /** Badge: ● ONLINE / ● ENFORCING etc. with color. */
113
+ export function badge(label, state, tone) {
114
+ const dot = "●";
115
+ return `${tone(`${dot} ${state}`)} ${dim(label)}`;
116
+ }
117
+
118
+ /** Truncate path for display, never show secret values. */
119
+ export function safeTarget(value, max = 44) {
120
+ const s = String(value ?? "");
121
+ if (s.length <= max) return s;
122
+ return "…" + s.slice(-(max - 1));
123
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Theme — semantic palette + box characters, with NO_COLOR and ASCII fallbacks.
3
+ *
4
+ * Never rely on color alone; every semantic token also has a text label
5
+ * (ALLOW/BLOCKED etc.) rendered elsewhere.
6
+ */
7
+
8
+ import { bold, dim, green, red, amber, blue, cyan, gray, stripAnsi, supportsUnicode } from "../format.mjs";
9
+
10
+ export const palette = {
11
+ success: green,
12
+ critical: red,
13
+ warning: amber,
14
+ info: blue,
15
+ muted: dim,
16
+ subtle: gray,
17
+ accent: cyan,
18
+ strong: bold,
19
+ };
20
+
21
+ export const riskTone = {
22
+ low: dim,
23
+ medium: blue,
24
+ high: amber,
25
+ critical: red,
26
+ };
27
+
28
+ export const decisionTone = {
29
+ allow: green,
30
+ sanitize: blue,
31
+ require_approval: amber,
32
+ deny: red,
33
+ audit_only: dim,
34
+ };
35
+
36
+ export function toneForDecision(decision) {
37
+ return decisionTone[String(decision ?? "").toLowerCase()] ?? dim;
38
+ }
39
+
40
+ export function toneForRisk(risk) {
41
+ return riskTone[String(risk ?? "").toLowerCase()] ?? dim;
42
+ }
43
+
44
+ /** Box characters, unicode primary, ascii fallback. */
45
+ export function boxChars() {
46
+ if (supportsUnicode()) {
47
+ return {
48
+ tl: "╭",
49
+ tr: "╮",
50
+ bl: "╰",
51
+ br: "╯",
52
+ h: "─",
53
+ v: "│",
54
+ lt: "├",
55
+ rt: "┤",
56
+ mt: "┬",
57
+ mb: "┴",
58
+ cross: "┼",
59
+ // heavy for intercept
60
+ hHeavy: "━",
61
+ vHeavy: "┃",
62
+ };
63
+ }
64
+ return {
65
+ tl: "+",
66
+ tr: "+",
67
+ bl: "+",
68
+ br: "+",
69
+ h: "-",
70
+ v: "|",
71
+ lt: "+",
72
+ rt: "+",
73
+ mt: "+",
74
+ mb: "+",
75
+ cross: "+",
76
+ hHeavy: "=",
77
+ vHeavy: "|",
78
+ };
79
+ }
80
+
81
+ /** Safe width truncation with ANSI stripped. */
82
+ export function truncate(str, n) {
83
+ const s = String(str ?? "");
84
+ if (s.length <= n) return s;
85
+ return s.slice(0, n - 1) + "…";
86
+ }
87
+
88
+ export function padVisible(str, width) {
89
+ const vis = stripAnsi(String(str)).length;
90
+ if (vis >= width) return String(str);
91
+ return String(str) + " ".repeat(width - vis);
92
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Cirvix Verified & MCP Trust Layer.
3
+ *
4
+ * Implements security verification for MCP servers and tools:
5
+ * - Provenance and publisher tracking
6
+ * - Tool poisoning / description tampering detection
7
+ * - Unpinned network egress analysis
8
+ * - Trust score computation (0-100)
9
+ * - Verification status: VERIFIED | REVIEW_REQUIRED | SUSPICIOUS | BLOCKED
10
+ */
11
+
12
+ export const VERIFICATION_STATUS = {
13
+ VERIFIED: "VERIFIED",
14
+ REVIEW_REQUIRED: "REVIEW_REQUIRED",
15
+ SUSPICIOUS: "SUSPICIOUS",
16
+ BLOCKED: "BLOCKED",
17
+ };
18
+
19
+ /**
20
+ * Scans an MCP server declaration and its tools for security risks and assigns trust score.
21
+ *
22
+ * @param {Object} server
23
+ * @param {string} server.name
24
+ * @param {string} [server.publisher]
25
+ * @param {string} [server.version]
26
+ * @param {Array} [server.tools]
27
+ * @param {Array} [server.destinations]
28
+ * @returns {Object} Inspection report
29
+ */
30
+ export function inspectMcpServer({
31
+ name,
32
+ publisher = null,
33
+ version = null,
34
+ tools = [],
35
+ destinations = [],
36
+ } = {}) {
37
+ let trustScore = 70; // Starting baseline for unverified server
38
+ const findings = [];
39
+
40
+ // Publisher trust
41
+ const knownTrustedPublishers = ["anthropic", "google", "github", "stripe", "aws", "cirvix"];
42
+ if (publisher && knownTrustedPublishers.includes(publisher.toLowerCase())) {
43
+ trustScore += 25;
44
+ } else if (!publisher) {
45
+ trustScore -= 15;
46
+ findings.push("Missing publisher provenance metadata");
47
+ }
48
+
49
+ // Network destinations check
50
+ for (const dest of destinations) {
51
+ const dLower = String(dest).toLowerCase();
52
+ if (dLower.includes("169.254.") || dLower.includes("metadata.google")) {
53
+ trustScore = 0;
54
+ findings.push(`Dangerous cloud metadata destination declared: ${dest}`);
55
+ return {
56
+ name,
57
+ trustScore: 0,
58
+ status: VERIFICATION_STATUS.BLOCKED,
59
+ findings,
60
+ };
61
+ }
62
+ }
63
+
64
+ // Tools inspection
65
+ for (const tool of tools) {
66
+ const desc = (tool.description ?? "").toLowerCase();
67
+ const toolName = (tool.name ?? "").toLowerCase();
68
+
69
+ // Check for tool poisoning / prompt injection markers in description
70
+ if (
71
+ desc.includes("ignore previous instructions") ||
72
+ desc.includes("system prompt") ||
73
+ desc.includes("do not tell the user") ||
74
+ desc.includes("send secrets to")
75
+ ) {
76
+ trustScore = 0;
77
+ findings.push(`Tool poisoning detected in tool '${tool.name}': suspicious prompt injection phrases in description`);
78
+ return {
79
+ name,
80
+ trustScore: 0,
81
+ status: VERIFICATION_STATUS.BLOCKED,
82
+ findings,
83
+ };
84
+ }
85
+
86
+ // Check for excessive wildcard permissions or raw shell execution
87
+ if (toolName === "bash" || toolName === "exec" || toolName === "eval") {
88
+ trustScore -= 20;
89
+ findings.push(`Tool '${tool.name}' exposes raw shell/process execution capability`);
90
+ }
91
+ }
92
+
93
+ trustScore = Math.max(0, Math.min(100, trustScore));
94
+
95
+ let status = VERIFICATION_STATUS.REVIEW_REQUIRED;
96
+ if (trustScore >= 85) status = VERIFICATION_STATUS.VERIFIED;
97
+ else if (trustScore < 50) status = VERIFICATION_STATUS.SUSPICIOUS;
98
+
99
+ return {
100
+ name,
101
+ publisher,
102
+ version,
103
+ trustScore,
104
+ status,
105
+ findings,
106
+ scannedAt: new Date().toISOString(),
107
+ };
108
+ }