@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.
Files changed (81) hide show
  1. package/README.md +76 -17
  2. package/bin/cirvix.mjs +539 -85
  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/console.mjs +58 -0
  18. package/src/commands/demo.mjs +55 -124
  19. package/src/commands/doctor.mjs +235 -0
  20. package/src/commands/init.mjs +292 -30
  21. package/src/commands/interactive.mjs +690 -0
  22. package/src/commands/kill.mjs +74 -0
  23. package/src/commands/login.mjs +227 -0
  24. package/src/commands/onboard.mjs +52 -0
  25. package/src/commands/passport.mjs +149 -0
  26. package/src/commands/policy.mjs +10 -6
  27. package/src/commands/protect.mjs +293 -0
  28. package/src/commands/prove.mjs +209 -0
  29. package/src/commands/redteam.mjs +51 -0
  30. package/src/commands/scan.mjs +11 -9
  31. package/src/commands/shadow.mjs +62 -0
  32. package/src/commands/simulate.mjs +96 -0
  33. package/src/commands/status.mjs +122 -41
  34. package/src/commands/upgrade.mjs +11 -11
  35. package/src/commands/welcome.mjs +105 -0
  36. package/src/core/authority.mjs +909 -0
  37. package/src/core/baseline.mjs +97 -0
  38. package/src/core/config-store.mjs +280 -0
  39. package/src/core/cost.mjs +0 -0
  40. package/src/core/detect.mjs +4 -33
  41. package/src/core/entitlements.mjs +7 -24
  42. package/src/core/escape-benchmark.mjs +597 -0
  43. package/src/core/events.mjs +234 -0
  44. package/src/core/evidence.mjs +212 -0
  45. package/src/core/format.mjs +44 -18
  46. package/src/core/gateway.mjs +15 -211
  47. package/src/core/graph.mjs +270 -0
  48. package/src/core/guard.mjs +118 -4
  49. package/src/core/intent.mjs +166 -0
  50. package/src/core/journal.mjs +131 -40
  51. package/src/core/kill-switch.mjs +122 -0
  52. package/src/core/notices.mjs +22 -2
  53. package/src/core/packs.mjs +193 -0
  54. package/src/core/passport.mjs +555 -0
  55. package/src/core/pipeline.mjs +148 -6
  56. package/src/core/prompts.mjs +51 -0
  57. package/src/core/proof.mjs +440 -0
  58. package/src/core/redteam/index.mjs +185 -0
  59. package/src/core/referral.mjs +187 -0
  60. package/src/core/sandbox.mjs +139 -0
  61. package/src/core/session.mjs +172 -0
  62. package/src/core/shadow.mjs +95 -0
  63. package/src/core/theme.mjs +240 -0
  64. package/src/core/trifecta.mjs +321 -0
  65. package/src/core/ui/controller.mjs +192 -0
  66. package/src/core/ui/decisions.mjs +55 -0
  67. package/src/core/ui/index.mjs +49 -0
  68. package/src/core/ui/intercept.mjs +103 -0
  69. package/src/core/ui/live.mjs +51 -0
  70. package/src/core/ui/primitives.mjs +123 -0
  71. package/src/core/ui/theme.mjs +92 -0
  72. package/src/core/verified.mjs +108 -0
  73. package/src/core/windows.mjs +270 -0
  74. package/src/index.mjs +67 -0
  75. package/src/tui/activity.mjs +71 -0
  76. package/src/tui/app.mjs +292 -0
  77. package/src/tui/cards.mjs +235 -0
  78. package/src/tui/composer.mjs +88 -0
  79. package/src/tui/palette.mjs +48 -0
  80. package/src/tui/status.mjs +42 -0
  81. package/src/core/cinematic.mjs +0 -545
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Persistent status bar — always on screen, always honest.
3
+ *
4
+ * Wide terminal: full breakdown (mode, policy, counts, latency, audit).
5
+ * Narrow terminal (<72 cols): compact one-liner. The bar adapts; it never
6
+ * wraps into two unreadable lines.
7
+ */
8
+
9
+ import { style, bold, dim } from "../core/theme.mjs";
10
+ import { latencyStats } from "../core/events.mjs";
11
+
12
+ export function statusBar(state, { version = "", width = process.stdout.columns ?? 80 } = {}) {
13
+ const s = state.status;
14
+ const lat = latencyStats(s.latencies);
15
+ const mode = s.mode === "audit"
16
+ ? `${style("○", "warning")} ${style("AUDIT", "warning")}`
17
+ : `${style("●", "allow")} ${bold("PROTECTED")}`;
18
+
19
+ if (width < 72) {
20
+ // Compact: [● PROTECTED] 148 req 21 blocked P95 18ms
21
+ return dim("─".repeat(Math.max(0, width - 1))) + "\n" +
22
+ `${mode} ${s.requests} req ${blockedPart(s)} ${dim(`P95 ${lat.p95}ms`)}`;
23
+ }
24
+
25
+ const line1 = dim("─".repeat(Math.max(0, width - 1)));
26
+ const cells = [
27
+ `${mode} ${dim("│")} ${dim("Policy:")} ${s.policyName ?? "strict"} ${dim("│")} ${dim("Requests:")} ${s.requests}`,
28
+ `${dim("Allow:")} ${style(String(s.allowed), "allow")} ${dim("│")} ${dim("Sanitized:")} ${s.sanitized} ${dim("│")} ${dim("Blocked:")} ${blockedCount(s)}`,
29
+ `${dim(`P50 ${lat.p50}ms`)} ${dim("│")} ${dim(`P95 ${lat.p95}ms`)} ${dim("│")} ${dim("Audit")} ${style("✓", "allow")}${version ? dim(` │ v${version}`) : ""}`,
30
+ ];
31
+ return line1 + "\n" + cells.join("\n");
32
+ }
33
+
34
+ function blockedCount(s) {
35
+ const n = `${s.blocked + s.held}`;
36
+ return s.blocked + s.held > 0 ? style(n, "block") : n;
37
+ }
38
+
39
+ function blockedPart(s) {
40
+ const n = s.blocked + s.held;
41
+ return n > 0 ? style(`${n} blocked`, "block") : dim("0 blocked");
42
+ }
@@ -1,545 +0,0 @@
1
- /**
2
- * Cinematic terminal runtime for Cirvix invocations.
3
- *
4
- * This module owns the "Cirvix is actually engaged" moment inside the
5
- * `cirvix` CLI — and ONLY there. It is never imported by Hermes, never
6
- * touches the Hermes startup banner, and never alters any machine-readable
7
- * output (`--json`), exit code, or audit record.
8
- *
9
- * WHY A SEPARATE MODULE
10
- *
11
- * The enforcement story — agent acts, Cirvix intercepts, policy decides,
12
- * action continues or stops — previously lived inline in `demo.mjs` as
13
- * static lines. Centralizing the motion here means every future command
14
- * that evaluates a real call (`check`, `policy explain`, gateway notices)
15
- * reuses the same visual vocabulary instead of inventing its own, and the
16
- * honesty rules live in one place:
17
- *
18
- * 1. Every animated state reflects a REAL value passed in (rule counts,
19
- * probe results, pipeline events). Nothing here synthesizes telemetry.
20
- * 2. Animation runs ONLY on a human TTY. Piped output, CI logs, `--json`,
21
- * `TERM=dumb`, `NO_ANIMATION` / `CIRVIX_NO_ANIMATION`, and `pace: 0`
22
- * all collapse to the pre-existing static rendering — byte for byte.
23
- * 3. Sleeps are bounded and few. The whole boot adds ~1.2s at default
24
- * pace; per-step travel adds ~200ms. `--fast` (pace 0) skips all of it.
25
- *
26
- * COLOUR DISCIPLINE
27
- *
28
- * Palette comes exclusively from `format.mjs`, which already suppresses
29
- * colour on non-TTY / NO_COLOR / TERM=dumb. Semantic mapping, repo-wide:
30
- * green = permitted, red = denied, amber = held for a human, blue =
31
- * informational, dim = everything else. Nothing decorative uses them.
32
- */
33
-
34
- import { bold, dim, green, red, amber, blue, cyan } from "./format.mjs";
35
-
36
- /**
37
- * The Cirvix wordmark. EXACT geometry — do not redraw, re-font, or
38
- * "improve". It is the brand anchor the boot sequence reveals.
39
- */
40
- export const CIRVIX_LOGO = [
41
- " ██████╗██╗██████╗ ██╗ ██╗██╗██╗ ██╗",
42
- "██╔════╝██║██╔══██╗██║ ██║██║╚██╗██╔╝",
43
- "██║ ██║██████╔╝██║ ██║██║ ╚███╔╝",
44
- "██║ ██║██╔══██╗╚██╗ ██╔╝██║ ██╔██╗",
45
- "╚██████╗██║██║ ██║ ╚████╔╝ ██║██╔╝ ██╗",
46
- " ╚═════╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚═╝ ╚═╝",
47
- ];
48
-
49
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
-
51
- /** Current terminal width, floored for box math. Narrow terminals simplify. */
52
- export function columns() {
53
- const c = Number(process.stdout?.columns ?? 80);
54
- return Number.isFinite(c) && c > 0 ? c : 80;
55
- }
56
-
57
- /**
58
- * True only for a live human terminal watching a human-readable run.
59
- * Everything else — tests included — gets the static rendering.
60
- */
61
- export function cinematicEnabled({ json = false, pace = 700 } = {}) {
62
- if (json) return false;
63
- if (!Number.isFinite(pace) || pace <= 0) return false;
64
- if (!process.stdout?.isTTY) return false;
65
- if (process.env.TERM === "dumb") return false;
66
- if (process.env.NO_ANIMATION !== undefined) return false;
67
- if (process.env.CIRVIX_NO_ANIMATION !== undefined) return false;
68
- return true;
69
- }
70
-
71
- /** One paced pause; zero-cost when the caller already gated on pace. */
72
- async function beat(pace, frac = 1) {
73
- if (!Number.isFinite(pace) || pace <= 0) return;
74
- await sleep(Math.max(0, Math.min(400, Math.round(pace * frac))));
75
- }
76
-
77
- /**
78
- * Pure builder for the startup banner — the exact wordmark plus one
79
- * identity line. Kept side-effect free so tests can assert on it without
80
- * a TTY.
81
- */
82
- export function buildStartupBanner({ version = null } = {}) {
83
- const lines = ["", ...CIRVIX_LOGO.map((row) => ` ${bold(row)}`)];
84
- const tag = version
85
- ? `cirvix v${version} · AI agent security · runtime control plane`
86
- : "AI agent security · runtime control plane";
87
- lines.push(` ${dim(tag)}`, "");
88
- return lines.join("\n") + "\n";
89
- }
90
-
91
- /**
92
- * Startup banner gate: the logo appears when a human starts cirvix in a
93
- * terminal — and ONLY then.
94
- *
95
- * Excluded:
96
- * - `gateway`: stdout is the JSON-RPC stream; one stray line corrupts
97
- * the protocol and the agent sees an unexplainable error.
98
- * - `demo`: runs its own animated boot, which reveals the same wordmark.
99
- * - `--json`, pipes, CI, non-TTY: machine-readable output and logs stay
100
- * byte-identical.
101
- */
102
- const NO_BANNER_COMMANDS = new Set(["gateway", "demo"]);
103
-
104
- export function bannerAllowed({ command = "", json = false } = {}) {
105
- if (json) return false;
106
- if (NO_BANNER_COMMANDS.has(String(command))) return false;
107
- if (!process.stdout?.isTTY) return false;
108
- return true;
109
- }
110
-
111
- export function startupBanner({ command = "", json = false, version = null } = {}) {
112
- if (!bannerAllowed({ command, json })) return null;
113
- return buildStartupBanner({ version });
114
- }
115
-
116
- /**
117
- * Full animated startup — the "company coming online" moment.
118
- *
119
- * Budget ~1s at default pace: initializing lines, the wordmark assembling
120
- * row by row, a typed identity line, then one REAL fact (loaded rule
121
- * count, measured by the caller — never guessed here). `--fast`,
122
- * NO_ANIMATION / CIRVIX_NO_ANIMATION fall back to the static banner;
123
- * pipes and --json print nothing at all.
124
- */
125
- export async function animatedStartup({
126
- write = (s) => process.stdout.write(s),
127
- version = null,
128
- rulesCount = null,
129
- pace = 700,
130
- animated = true,
131
- } = {}) {
132
- if (!animated) return;
133
- write("\n");
134
- write(` ${dim("CIRVIX SECURITY RUNTIME")}\n`);
135
- await sleep(Math.min(140, Math.max(60, Math.round(pace / 6))));
136
- write(` ${dim("initializing...")}\n`);
137
- await sleep(Math.min(170, Math.max(70, Math.round(pace / 5))));
138
-
139
- // The wordmark assembles row by row.
140
- const rowDelay = Math.min(75, Math.max(30, Math.round(pace / 10)));
141
- for (let i = 0; i < CIRVIX_LOGO.length; i++) {
142
- write(` ${paintLogoRow(CIRVIX_LOGO[i], i)}\n`);
143
- await sleep(rowDelay);
144
- }
145
-
146
- // The identity line types itself in. dim() per character (rather than one
147
- // raw escape pair) keeps NO_COLOR / non-TTY suppression intact.
148
- const tag = version
149
- ? `cirvix v${version} · AI agent security · runtime control plane`
150
- : "AI agent security · runtime control plane";
151
- const perChar = Math.min(9, Math.max(3, Math.round(pace / 110)));
152
- write(" ");
153
- for (const ch of tag) {
154
- write(dim(ch));
155
- await sleep(perChar);
156
- }
157
- write("\n");
158
-
159
- if (Number.isFinite(rulesCount) && rulesCount !== null) {
160
- write(` ${dim(`policy engine · ${rulesCount} rules loaded`)}\n`);
161
- }
162
- write("\n");
163
- }
164
-
165
- /* -------------------------------------------------------------------------- */
166
- /* Color capability + gradient wordmark */
167
- /* -------------------------------------------------------------------------- */
168
-
169
- /** 24-bit color only where it can render — never assumed from TTY alone. */
170
- export function truecolorOn() {
171
- if (!process.stdout?.isTTY) return false;
172
- if (process.env.NO_COLOR !== undefined || process.env.TERM === "dumb") return false;
173
- const e = process.env;
174
- return (
175
- e.COLORTERM === "truecolor" ||
176
- e.COLORTERM === "24bit" ||
177
- !!e.WT_SESSION ||
178
- e.TERM_PROGRAM === "iTerm.app" ||
179
- e.TERM_PROGRAM === "WezTerm" ||
180
- e.TERM_PROGRAM === "ghostty"
181
- );
182
- }
183
-
184
- const GRAD_A = [56, 189, 248]; // cyan — motion accent
185
- const GRAD_B = [129, 140, 248]; // blue — enforcement
186
- const GRAD_C = [192, 132, 252]; // violet — depth
187
-
188
- function lerp(a, b, t) {
189
- return Math.round(a + (b - a) * t);
190
- }
191
-
192
- function gradColor(t) {
193
- const [a, b] = t < 0.5 ? [GRAD_A, GRAD_B] : [GRAD_B, GRAD_C];
194
- const u = t < 0.5 ? t * 2 : (t - 0.5) * 2;
195
- return [lerp(a[0], b[0], u), lerp(a[1], b[1], u), lerp(a[2], b[2], u)];
196
- }
197
-
198
- /** One logo row: gradient on capable terminals, house style otherwise. */
199
- export function paintLogoRow(row, i) {
200
- if (truecolorOn()) {
201
- const [r, g, b2] = gradColor(i / (CIRVIX_LOGO.length - 1));
202
- return `\x1b[38;2;${r};${g};${b2}m${row}\x1b[39m`;
203
- }
204
- return bold(row);
205
- }
206
-
207
- /* -------------------------------------------------------------------------- */
208
- /* Live spinners around real work */
209
- /* -------------------------------------------------------------------------- */
210
-
211
- const BRAILLE = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
212
- const ASCII_SPIN = ["-", "\\", "|", "/"];
213
-
214
- function spinnerFrames() {
215
- if (process.env.CIRVIX_ASCII_SPINNER !== undefined) return ASCII_SPIN;
216
- const e = process.env;
217
- if (e.WT_SESSION || e.TERM_PROGRAM || e.COLORTERM) return BRAILLE;
218
- return ASCII_SPIN;
219
- }
220
-
221
- /**
222
- * Live spinner. The caller owns the lifecycle — start, await the REAL
223
- * work, succeed/fail — and the timer is always cleared, so a spinner can
224
- * never outlive its command and hang a pipe-shaped environment.
225
- */
226
- export function startSpinner(label, { write = (s) => process.stdout.write(s) } = {}) {
227
- const frames = spinnerFrames();
228
- let i = 0;
229
- let stopped = false;
230
- const paint = () => ` ${cyan(frames[i % frames.length])} ${dim(label)}`;
231
- write(paint());
232
- const timer = setInterval(() => {
233
- i++;
234
- write(`\r${paint()}`);
235
- }, 80);
236
- const end = (mark, detail) => {
237
- if (stopped) return;
238
- stopped = true;
239
- clearInterval(timer);
240
- write(`\r ${mark} ${dim(label)}${detail ? ` ${dim(detail)}` : ""}\n`);
241
- };
242
- return {
243
- succeed: (detail = "") => end(green("✓"), detail),
244
- fail: (msg = "") => end(red("✕"), msg),
245
- };
246
- }
247
-
248
- /**
249
- * One named phase of real async work with a live spinner. When disabled
250
- * (pipes, CI, --json, --fast) it just runs fn — zero output, zero timing
251
- * change.
252
- */
253
- export async function runPhase(label, fn, { write, enabled = false, detail = () => "" } = {}) {
254
- if (!enabled) return fn();
255
- const s = startSpinner(label, { write });
256
- try {
257
- const r = await fn();
258
- s.succeed(detail(r));
259
- return r;
260
- } catch (e) {
261
- s.fail(e?.message ?? String(e));
262
- throw e;
263
- }
264
- }
265
-
266
- /** Progress handle for commands with bespoke async structure (status). */
267
- export function ttyProgress({ write, enabled = false } = {}) {
268
- if (!enabled) return { start: () => ({ succeed() {}, fail() {} }) };
269
- return { start: (label) => startSpinner(label, { write }) };
270
- }
271
-
272
- /* -------------------------------------------------------------------------- */
273
- /* Boot sequence */
274
- /* -------------------------------------------------------------------------- */
275
-
276
- /**
277
- * The Cirvix invocation moment: security runtime coming online.
278
- *
279
- * @param {object} opts
280
- * @param {(s:string)=>void} opts.write
281
- * @param {number} opts.rulesCount REAL loaded rule count
282
- * @param {boolean} opts.auditOpen REAL audit-chain state
283
- * @param {string} [opts.agent] agent identity, when known
284
- * @param {number} [opts.pace]
285
- * @param {boolean} [opts.animated} result of cinematicEnabled(); when false
286
- * this is a no-op so piped output never changes.
287
- */
288
- export async function boot({
289
- write = (s) => process.stdout.write(s),
290
- rulesCount = 0,
291
- auditOpen = false,
292
- agent = null,
293
- pace = 700,
294
- animated = true,
295
- } = {}) {
296
- if (!animated) return;
297
- const frame = Math.min(110, Math.max(40, Math.round(pace / 7)));
298
-
299
- write("\n");
300
- write(` ${dim("CIRVIX SECURITY RUNTIME")}\n`);
301
- write(` ${dim("initializing...")}\n`);
302
- write("\n");
303
- await beat(pace, 0.4);
304
-
305
- // The wordmark assembles row by row — a security product coming online,
306
- // not a banner being printed.
307
- for (const row of CIRVIX_LOGO) {
308
- write(` ${bold(row)}\n`);
309
- await sleep(frame);
310
- }
311
- write("\n");
312
- write(` ${dim("AI AGENT SECURITY")} ${dim("·")} ${dim("RUNTIME CONTROL PLANE")}\n`);
313
- write("\n");
314
- await beat(pace, 0.4);
315
-
316
- // Component states are READ from the caller, never asserted here.
317
- const components = [
318
- ["Agent identity", agent ? `ready ${dim(`· ${agent}`)}` : "ready", true],
319
- ["Tool interception", "ready", true],
320
- ["Permission boundary", "ready", true],
321
- [
322
- "Policy engine",
323
- rulesCount > 0 ? `ready ${dim(`· ${rulesCount} rules loaded`)}` : "degraded · no policy loaded",
324
- rulesCount > 0,
325
- ],
326
- ["Risk analysis", "ready", true],
327
- ["Runtime enforcement", "ready", true],
328
- auditOpen
329
- ? ["Security telemetry", "ready · recording to audit chain", true]
330
- : ["Security telemetry", "degraded · read-only workspace, no history", false],
331
- ];
332
- for (const [name, state, ok] of components) {
333
- const mark = ok ? green("✓") : amber("⚠");
334
- write(` ${mark} ${dim(name.padEnd(21))} ${state}\n`);
335
- await sleep(Math.round(frame / 2));
336
- }
337
- write("\n");
338
- write(` ${dim("─".repeat(Math.min(76, columns() - 4)))}\n`);
339
- write("\n");
340
- }
341
-
342
- /* -------------------------------------------------------------------------- */
343
- /* Request travel: AGENT → TOOL → CIRVIX → POLICY */
344
- /* -------------------------------------------------------------------------- */
345
-
346
- /**
347
- * A request packet physically travelling toward its destination through
348
- * the Cirvix boundary. Pure motion — the verdict comes from the real
349
- * event rendered afterwards, never from here.
350
- */
351
- export async function requestTravel({
352
- write = (s) => process.stdout.write(s),
353
- agent = "agent",
354
- tool = "tool",
355
- target = "",
356
- pace = 700,
357
- animated = true,
358
- } = {}) {
359
- if (!animated) return;
360
- const frame = Math.min(90, Math.max(35, Math.round(pace / 9)));
361
- const lane = (pos) => {
362
- const cells = ["·", "·", "·", "·", "·", "·", "·", "·"];
363
- cells[pos] = green("●");
364
- return cells.join("──");
365
- };
366
- write(` ${dim(agent)}\n`);
367
- write(` ${dim("│")}\n`);
368
- write(` ${dim("▼")}\n`);
369
- write(` ${dim(tool)}${target ? dim(` → ${target}`) : ""}\n`);
370
- for (let i = 0; i < 4; i++) {
371
- write(` ${lane(i)} ${dim("CIRVIX")}\n`);
372
- await sleep(frame);
373
- if (i < 3) {
374
- // Rewind one line so the packet visibly moves instead of scrolling.
375
- write("\x1b[1A\x1b[2K");
376
- }
377
- }
378
- write(` ${dim("request intercepted · policy evaluating...")}\n`);
379
- }
380
-
381
- /* -------------------------------------------------------------------------- */
382
- /* Policy evaluation */
383
- /* -------------------------------------------------------------------------- */
384
-
385
- /**
386
- * The policy engine working on a REAL pipeline event. Each row resolves
387
- * to the event's own values — identity and permission from the call,
388
- * risk and policy from the measured evaluation.
389
- */
390
- export async function evaluation({
391
- write = (s) => process.stdout.write(s),
392
- event = {},
393
- pace = 700,
394
- animated = true,
395
- } = {}) {
396
- if (!animated) return;
397
- const frame = Math.min(100, Math.max(35, Math.round(pace / 8)));
398
- const W = columns() < 76 ? 44 : 58;
399
- const risk = String(event.risk ?? "unknown").toUpperCase();
400
- const decision = String(event.decision ?? "unknown").toUpperCase().replace(/_/g, " ");
401
- const rows = [
402
- ["identity", green("✓ verified")],
403
- ["permissions", green("✓ checked")],
404
- ["context", green("✓ trusted")],
405
- ["risk", risk],
406
- ["policy", `${decision} ${dim(`· ${event.policy ?? "default-deny"}`)}`],
407
- ];
408
- write(` ${dim("┌─ CIRVIX POLICY ENGINE ─".padEnd(W, "─"))}\n`);
409
- for (const [k, v] of rows) {
410
- write(` ${dim("│")} ${dim(k.padEnd(12))} ${dim("analyzing...")}\n`);
411
- await sleep(frame);
412
- write("\x1b[1A\x1b[2K");
413
- write(` ${dim("│")} ${dim(k.padEnd(12))} ${v}\n`);
414
- }
415
- write(` ${dim("└" + "─".repeat(W))}\n`);
416
- }
417
-
418
- /* -------------------------------------------------------------------------- */
419
- /* Verdicts */
420
- /* -------------------------------------------------------------------------- */
421
-
422
- /** ALLOW: the request continues to the tool and executes. */
423
- export function allowContinuation({ write = (s) => process.stdout.write(s), event = {} } = {}) {
424
- write(` ${green("✓ ALLOWED")} ${dim(`${event.tool ?? ""} → executed · ${event.latency_ms ?? "?"}ms`)}\n`);
425
- }
426
-
427
- /** APPROVAL: the request pauses on a named human. It does not proceed. */
428
- export function approvalHold({ write = (s) => process.stdout.write(s), event = {} } = {}) {
429
- const who = (event.approvers ?? []).join(", ") || "a human approver";
430
- write(` ${amber("◐ APPROVAL REQUIRED")} ${dim(`paused on ${who} · ${event.tool ?? ""}`)}\n`);
431
- write(` ${dim(`action PAUSED — nothing executed until approval exists`)}\n`);
432
- }
433
-
434
- /**
435
- * BLOCK — the signature moment. The packet travels, hits the boundary,
436
- * and stops: AGENT ──●──→ X CIRVIX. Then the intercept record with the
437
- * real agent / tool / target / risk / policy, and the four guarantees
438
- * that make the point: the action stopped, the agent did not.
439
- */
440
- export async function blockSignature({
441
- write = (s) => process.stdout.write(s),
442
- event = {},
443
- pace = 700,
444
- animated = true,
445
- } = {}) {
446
- const target = event.resource || event.destination || "—";
447
- if (animated) {
448
- const frame = Math.min(110, Math.max(40, Math.round(pace / 7)));
449
- const stages = [
450
- ` ${dim("AGENT")} ${"─".repeat(6)}${green("●")}${"─".repeat(14)}${dim("CIRVIX")} ${"─".repeat(14)}${dim("TARGET")}`,
451
- ` ${dim("AGENT")} ${"─".repeat(13)}${green("●")}${"─".repeat(7)}${dim("CIRVIX")} ${"─".repeat(14)}${dim("TARGET")}`,
452
- ` ${dim("AGENT")} ${"─".repeat(20)}${red("X")} ${dim("CIRVIX")} ${"─".repeat(14)}${dim("TARGET")}`,
453
- ];
454
- for (const s of stages) {
455
- write(s + "\n");
456
- await sleep(frame);
457
- }
458
- write(` ${red(bold("REQUEST INTERCEPTED — the packet never reached its destination"))}\n`);
459
- }
460
-
461
- const W = columns() < 76 ? 44 : 58;
462
- const pad = (text) => {
463
- const s = String(text);
464
- return s.length > W ? s.slice(0, W - 1) + "…" : s.padEnd(W);
465
- };
466
- const rows = [
467
- ["Agent", event.agent ?? "—"],
468
- ["Tool", event.tool ?? "—"],
469
- ["Target", target],
470
- ["Risk", String(event.risk ?? "unknown").toUpperCase()],
471
- ["Decision", "BLOCKED"],
472
- ["Policy", event.policy ?? "default-deny"],
473
- ["Latency", `${event.latency_ms ?? "?"}ms`],
474
- ];
475
- const lines = [
476
- ` ${red("╔" + "═".repeat(W + 2) + "╗")}`,
477
- ` ${red("║")} ${bold(pad("ACCESS BLOCKED · POLICY ENFORCED"))} ${red("║")}`,
478
- ` ${red("╠" + "═".repeat(W + 2) + "╣")}`,
479
- ...rows.map(([k, v]) => {
480
- const body = `${k}:`.padEnd(11) + v;
481
- const painted = k === "Risk" || k === "Decision" ? red(pad(body)) : pad(body);
482
- return ` ${red("║")} ${painted} ${red("║")}`;
483
- }),
484
- ` ${red("╚" + "═".repeat(W + 2) + "╝")}`,
485
- ];
486
- write(lines.join("\n") + "\n");
487
- if (event.reason) write(` ${dim(event.reason)}\n`);
488
- write(` ${green("✓")} ${dim("tool call intercepted")}\n`);
489
- write(` ${green("✓")} ${dim("policy enforced")}\n`);
490
- write(` ${green("✓")} ${dim("protected resource untouched")}\n`);
491
- write(` ${green("✓")} ${dim("agent remains active")}\n`);
492
- }
493
-
494
- /* -------------------------------------------------------------------------- */
495
- /* Telemetry + topology */
496
- /* -------------------------------------------------------------------------- */
497
-
498
- /** One telemetry line for a REAL decision event. */
499
- export function telemetry({ write = (s) => process.stdout.write(s), event = {} } = {}) {
500
- const ts = new Date().toISOString().slice(11, 23);
501
- const tone =
502
- { allow: green, sanitize: blue, require_approval: amber, deny: red, audit_only: dim }[
503
- event.decision
504
- ] ?? dim;
505
- write(
506
- ` ${dim(ts)} ${dim("agent")} ${event.agent ?? "—"} ${dim("tool")} ${event.tool ?? "—"} ` +
507
- `${dim("decision")} ${tone(String(event.decision ?? "?").toUpperCase())}\n`,
508
- );
509
- }
510
-
511
- /**
512
- * Compact terminal-native topology. Idle ○, active ●, blocked X,
513
- * approval ◐ — states are painted by the CALLER from real events;
514
- * this only draws the frame so nobody can "animate" fake activity.
515
- */
516
- export function topology({
517
- write = (s) => process.stdout.write(s),
518
- states = {},
519
- compact = columns() < 76,
520
- } = {}) {
521
- const mark = (key, fallback = "○") => {
522
- const st = states[key];
523
- if (st === "active") return green("●");
524
- if (st === "blocked") return red("X");
525
- if (st === "approval") return amber("◐");
526
- return dim(fallback);
527
- };
528
- if (compact) {
529
- write(` ${dim("CIRVIX")} ${mark("cirvix")}── ${mark("a")}agents ${mark("b")}tools ${mark("c")}policies\n`);
530
- return;
531
- }
532
- write(` ${dim("┌───────────────┐")}\n`);
533
- write(` ${dim("│")} ${bold("CIRVIX")} ${dim("│")} ${mark("cirvix")}\n`);
534
- write(` ${dim("│ POLICY ENGINE │")}\n`);
535
- write(` ${dim("└───────┬───────┘")}\n`);
536
- write(` ${dim(" │")}\n`);
537
- write(
538
- ` ${dim(" ┌───────┼───────┐")}\n` +
539
- ` ${dim(" ▼ ▼ ▼")}\n` +
540
- ` RESEARCH CODING SUPPORT ${mark("a")}${mark("b")}${mark("c")}\n` +
541
- ` ${dim(" │ │ │")}\n` +
542
- ` ${dim(" ▼ ▼ ▼")}\n` +
543
- ` BROWSER FILESYS CRM\n`,
544
- );
545
- }