@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
|
@@ -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
|
+
}
|
package/src/core/cinematic.mjs
DELETED
|
@@ -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
|
-
}
|