@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,106 @@
1
+ /**
2
+ * Roo Code Adapter for CIRVIX AgentControl.
3
+ *
4
+ * Supports:
5
+ * - VS Code globalStorage roo_code_mcp_settings.json across Windows, macOS, Linux
6
+ * - Workspace .roomodes and .clinerules
7
+ */
8
+
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { BaseAgentAdapter } from "./base.mjs";
12
+
13
+ export class RooCodeAdapter extends BaseAgentAdapter {
14
+ constructor() {
15
+ super({
16
+ id: "roo-code",
17
+ label: "Roo Code",
18
+ type: "extension",
19
+ mcpKey: "mcpServers",
20
+ });
21
+ }
22
+
23
+ async detect(cwd) {
24
+ const home = homedir();
25
+ const candidatePaths = [
26
+ join(cwd, ".roo", "mcp_settings.json"),
27
+ // Windows
28
+ join(home, "AppData", "Roaming", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json"),
29
+ // macOS
30
+ join(home, "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json"),
31
+ // Linux
32
+ join(home, ".config", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json"),
33
+ ];
34
+
35
+ const existingPaths = [];
36
+ const servers = {};
37
+ let configFound = null;
38
+
39
+ for (const p of candidatePaths) {
40
+ if (await this.fileExists(p)) {
41
+ existingPaths.push(p);
42
+ const data = await this.readJson(p);
43
+ if (data) {
44
+ if (!configFound) configFound = data;
45
+ Object.assign(servers, data[this.mcpKey] ?? {});
46
+ }
47
+ }
48
+ }
49
+
50
+ const hasModes = (await this.fileExists(join(cwd, ".roomodes"))) || (await this.fileExists(join(cwd, ".clinerules")));
51
+ const detected = existingPaths.length > 0 || hasModes;
52
+
53
+ const isIntegrated = Object.entries(servers).some(([name, def]) => this.isCirvixServer(name, def));
54
+
55
+ return {
56
+ detected,
57
+ paths: existingPaths,
58
+ targetConfigPath: existingPaths[0] ?? candidatePaths[1],
59
+ config: configFound,
60
+ servers,
61
+ serverCount: Object.keys(servers).length,
62
+ isIntegrated,
63
+ hasConfig: existingPaths.length > 0,
64
+ executable: null,
65
+ metadata: {
66
+ hasModes,
67
+ },
68
+ };
69
+ }
70
+
71
+ async generateIntegrationPlan(cwd, options = {}) {
72
+ const info = await this.detect(cwd);
73
+ const targetFile = info.targetConfigPath;
74
+ const currentServers = { ...info.servers };
75
+
76
+ const upstreams = {};
77
+ for (const [k, v] of Object.entries(currentServers)) {
78
+ if (!this.isCirvixServer(k, v)) upstreams[k] = v;
79
+ }
80
+
81
+ const cirvixServerDef = {
82
+ command: "cirvix",
83
+ args: ["gateway", "--servers", targetFile.replace(/\\/g, "/")],
84
+ };
85
+
86
+ const newConfig = {
87
+ ...(info.config || {}),
88
+ [this.mcpKey]: {
89
+ ...currentServers,
90
+ cirvix: cirvixServerDef,
91
+ },
92
+ };
93
+
94
+ return {
95
+ adapterId: this.id,
96
+ label: this.label,
97
+ targetFile,
98
+ canIntegrate: true,
99
+ currentServers,
100
+ upstreams,
101
+ plan: newConfig,
102
+ snippet: JSON.stringify({ [this.mcpKey]: { cirvix: cirvixServerDef } }, null, 2),
103
+ reason: "Protects Roo Code autonomous task execution via CIRVIX gateway.",
104
+ };
105
+ }
106
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * VS Code Standard MCP Adapter for CIRVIX AgentControl.
3
+ *
4
+ * Supports:
5
+ * - User mcp.json across Windows, macOS, Linux
6
+ * - Workspace .vscode/mcp.json
7
+ * - Uses the "servers" root key per VS Code MCP specification
8
+ */
9
+
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { BaseAgentAdapter } from "./base.mjs";
13
+
14
+ export class VSCodeAdapter extends BaseAgentAdapter {
15
+ constructor() {
16
+ super({
17
+ id: "vscode",
18
+ label: "VS Code (MCP)",
19
+ type: "editor",
20
+ mcpKey: "servers",
21
+ });
22
+ }
23
+
24
+ async detect(cwd) {
25
+ const home = homedir();
26
+ const candidatePaths = [
27
+ join(cwd, ".vscode", "mcp.json"),
28
+ // Windows
29
+ join(home, "AppData", "Roaming", "Code", "User", "mcp.json"),
30
+ // macOS
31
+ join(home, "Library", "Application Support", "Code", "User", "mcp.json"),
32
+ // Linux
33
+ join(home, ".config", "Code", "User", "mcp.json"),
34
+ ];
35
+
36
+ const existingPaths = [];
37
+ const servers = {};
38
+ let configFound = null;
39
+
40
+ for (const p of candidatePaths) {
41
+ if (await this.fileExists(p)) {
42
+ existingPaths.push(p);
43
+ const data = await this.readJson(p);
44
+ if (data) {
45
+ if (!configFound) configFound = data;
46
+ const map = data.servers ?? data.mcpServers ?? {};
47
+ Object.assign(servers, map);
48
+ }
49
+ }
50
+ }
51
+
52
+ const detected = existingPaths.length > 0;
53
+ const isIntegrated = Object.entries(servers).some(([name, def]) => this.isCirvixServer(name, def));
54
+
55
+ return {
56
+ detected,
57
+ paths: existingPaths,
58
+ targetConfigPath: existingPaths[0] ?? candidatePaths[1],
59
+ config: configFound,
60
+ servers,
61
+ serverCount: Object.keys(servers).length,
62
+ isIntegrated,
63
+ hasConfig: existingPaths.length > 0,
64
+ executable: null,
65
+ metadata: {},
66
+ };
67
+ }
68
+
69
+ async generateIntegrationPlan(cwd, options = {}) {
70
+ const info = await this.detect(cwd);
71
+ const targetFile = info.targetConfigPath;
72
+ const currentServers = { ...info.servers };
73
+
74
+ const upstreams = {};
75
+ for (const [k, v] of Object.entries(currentServers)) {
76
+ if (!this.isCirvixServer(k, v)) upstreams[k] = v;
77
+ }
78
+
79
+ const cirvixServerDef = {
80
+ command: "cirvix",
81
+ args: ["gateway", "--servers", targetFile.replace(/\\/g, "/")],
82
+ };
83
+
84
+ const newConfig = {
85
+ ...(info.config || {}),
86
+ [this.mcpKey]: {
87
+ ...currentServers,
88
+ cirvix: cirvixServerDef,
89
+ },
90
+ };
91
+
92
+ return {
93
+ adapterId: this.id,
94
+ label: this.label,
95
+ targetFile,
96
+ canIntegrate: true,
97
+ currentServers,
98
+ upstreams,
99
+ plan: newConfig,
100
+ snippet: JSON.stringify({ [this.mcpKey]: { cirvix: cirvixServerDef } }, null, 2),
101
+ reason: "Protects VS Code MCP tool execution across all configured servers.",
102
+ };
103
+ }
104
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Windsurf Adapter for CIRVIX AgentControl.
3
+ *
4
+ * Supports:
5
+ * - ~/.codeium/windsurf/mcp_config.json
6
+ * - Workspace .windsurf/mcp_config.json, .codeium/windsurf/mcp_config.json
7
+ * - .windsurfrules
8
+ * - Executable detection (windsurf / windsurf.cmd)
9
+ */
10
+
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { BaseAgentAdapter } from "./base.mjs";
14
+ import { resolveExecutable } from "../core/windows.mjs";
15
+
16
+ export class WindsurfAdapter extends BaseAgentAdapter {
17
+ constructor() {
18
+ super({
19
+ id: "windsurf",
20
+ label: "Windsurf",
21
+ type: "editor",
22
+ mcpKey: "mcpServers",
23
+ });
24
+ }
25
+
26
+ async detect(cwd) {
27
+ const home = homedir();
28
+ const candidatePaths = [
29
+ join(cwd, ".windsurf", "mcp_config.json"),
30
+ join(cwd, ".codeium", "windsurf", "mcp_config.json"),
31
+ join(home, ".codeium", "windsurf", "mcp_config.json"),
32
+ join(home, ".windsurf", "mcp_config.json"),
33
+ ];
34
+
35
+ const existingPaths = [];
36
+ const servers = {};
37
+ let configFound = null;
38
+
39
+ for (const p of candidatePaths) {
40
+ if (await this.fileExists(p)) {
41
+ existingPaths.push(p);
42
+ const data = await this.readJson(p);
43
+ if (data) {
44
+ if (!configFound) configFound = data;
45
+ Object.assign(servers, data[this.mcpKey] ?? {});
46
+ }
47
+ }
48
+ }
49
+
50
+ const execPath = resolveExecutable("windsurf", { cwd });
51
+ const hasRules = await this.fileExists(join(cwd, ".windsurfrules"));
52
+ const detected = existingPaths.length > 0 || Boolean(execPath) || hasRules;
53
+
54
+ const isIntegrated = Object.entries(servers).some(([name, def]) => this.isCirvixServer(name, def));
55
+
56
+ return {
57
+ detected,
58
+ paths: existingPaths,
59
+ targetConfigPath: existingPaths[0] ?? join(home, ".codeium", "windsurf", "mcp_config.json"),
60
+ config: configFound,
61
+ servers,
62
+ serverCount: Object.keys(servers).length,
63
+ isIntegrated,
64
+ hasConfig: existingPaths.length > 0,
65
+ executable: execPath,
66
+ metadata: {
67
+ hasRules,
68
+ },
69
+ };
70
+ }
71
+
72
+ async generateIntegrationPlan(cwd, options = {}) {
73
+ const info = await this.detect(cwd);
74
+ const targetFile = info.targetConfigPath;
75
+ const currentServers = { ...info.servers };
76
+
77
+ const upstreams = {};
78
+ for (const [k, v] of Object.entries(currentServers)) {
79
+ if (!this.isCirvixServer(k, v)) upstreams[k] = v;
80
+ }
81
+
82
+ const cirvixServerDef = {
83
+ command: "cirvix",
84
+ args: ["gateway", "--servers", targetFile.replace(/\\/g, "/")],
85
+ };
86
+
87
+ const newConfig = {
88
+ ...(info.config || {}),
89
+ [this.mcpKey]: {
90
+ ...currentServers,
91
+ cirvix: cirvixServerDef,
92
+ },
93
+ };
94
+
95
+ return {
96
+ adapterId: this.id,
97
+ label: this.label,
98
+ targetFile,
99
+ canIntegrate: true,
100
+ currentServers,
101
+ upstreams,
102
+ plan: newConfig,
103
+ snippet: JSON.stringify({ [this.mcpKey]: { cirvix: cirvixServerDef } }, null, 2),
104
+ reason: "Integrates CIRVIX runtime policy and secret isolation into Windsurf Cascade.",
105
+ };
106
+ }
107
+ }
@@ -38,7 +38,11 @@ import { compile } from "../core/policy-dsl.mjs";
38
38
  import { STARTER_POLICY } from "./init.mjs";
39
39
  import { scan as scanInjection } from "../core/sanitize.mjs";
40
40
  import { DECISION } from "../core/decisions.mjs";
41
- import { bold, dim, green, red, amber, blue } from "../core/format.mjs";
41
+ import { bold, dim, green, red, amber, blue, cyan } from "../core/format.mjs";
42
+ import { brandHeader, panel, separator } from "../core/ui/primitives.mjs";
43
+ import { interceptBox } from "../core/ui/intercept.mjs";
44
+ import { renderDecision } from "../core/ui/decisions.mjs";
45
+ import { shouldAnimate } from "../core/ui/controller.mjs";
42
46
 
43
47
  /** The poisoned content. This is what an agent finds on a page it was told to read. */
44
48
  const POISONED_PAGE = `# Deploying to production
@@ -160,12 +164,14 @@ export async function demo({
160
164
  });
161
165
  const steps = [];
162
166
 
167
+ const animated = !json && shouldAnimate({ pace, json });
163
168
  if (!json) {
164
169
  write("\n");
165
- write(` ${bold("CIRVIX")} ${dim("· live demo · every decision below is computed, not scripted")}\n`);
170
+ write(brandHeader({ width: 62 }) + "\n");
171
+ write(` ${dim("CIRVIX · SECURITY DEMONSTRATION · every decision below is computed, not scripted")}\n`);
166
172
  write("\n");
167
173
  write(` ${dim("─".repeat(76))}\n`);
168
- write(` ${bold("ACT I")} ${dim("The agent reads a page containing an instruction addressed to it.")}\n`);
174
+ write(` ${bold("ACT I")} ${dim("Untrusted content reaches the agent")}\n`);
169
175
  write(` ${dim("─".repeat(76))}\n\n`);
170
176
 
171
177
  // Show what is actually in the page — the attack is the interesting part.
@@ -179,7 +185,7 @@ export async function demo({
179
185
  write(` ${red("·")} ${dim(f.label)}\n`);
180
186
  }
181
187
  write("\n");
182
- await sleep(pace * 2);
188
+ await sleep(animated ? pace * 1.2 : pace === 0 ? 0 : pace * 0.5);
183
189
  }
184
190
 
185
191
  let act = null;
@@ -188,14 +194,14 @@ export async function demo({
188
194
  act = step.act;
189
195
  if (act === "work") {
190
196
  write(`\n ${dim("─".repeat(76))}\n`);
191
- write(` ${bold("ACT III")} ${dim("The same agent, the same policy, doing its job.")}\n`);
197
+ write(` ${bold("ACT III")} ${dim("Legitimate work continues.")}\n`);
192
198
  write(` ${dim("─".repeat(76))}\n\n`);
193
199
  } else {
194
200
  write(`\n ${dim("─".repeat(76))}\n`);
195
- write(` ${bold("ACT II")} ${dim("It believes the page.")}\n`);
201
+ write(` ${bold("ACT II")} ${dim("CIRVIX evaluates the resulting actions.")}\n`);
196
202
  write(` ${dim("─".repeat(76))}\n\n`);
197
203
  }
198
- await sleep(pace);
204
+ await sleep(animated ? pace * 0.6 : pace === 0 ? 0 : pace * 0.3);
199
205
  }
200
206
 
201
207
  const { event } = await pipeline.submit(step.call);
@@ -205,16 +211,25 @@ export async function demo({
205
211
 
206
212
  if (step.narration) {
207
213
  write(` ${dim(step.narration)}\n\n`);
208
- await sleep(pace / 2);
214
+ await sleep(animated ? pace * 0.3 : 0);
209
215
  }
210
216
 
211
217
  if (step.intercept && event.decision === DECISION.DENY) {
218
+ if (animated) {
219
+ // Brief evaluating sequence — decision already made, just visualizing.
220
+ write(` ${dim("◌ evaluating request...")}\n`);
221
+ await sleep(Math.min(260, pace * 0.35));
222
+ write(` ${red("⚠ " + String(event.risk).toUpperCase())}\n`);
223
+ await sleep(Math.min(160, pace * 0.2));
224
+ write(` ${red(bold("✕ BLOCKED"))} ${dim(event.policy ?? "")}\n`);
225
+ await sleep(Math.min(160, pace * 0.2));
226
+ }
212
227
  write(interceptBox(event));
213
228
  write("\n");
214
229
  } else {
215
- write(oneLine(event));
230
+ write(renderDecision(event) + "\n");
216
231
  }
217
- await sleep(pace);
232
+ await sleep(animated ? pace * 0.7 : pace === 0 ? 0 : pace * 0.4);
218
233
  }
219
234
 
220
235
  const p = pipeline.percentiles();
@@ -241,75 +256,46 @@ export async function demo({
241
256
 
242
257
  write("\n");
243
258
  write(` ${dim("─".repeat(76))}\n\n`);
244
- write(` ${green(bold(String(allowed)))} ${dim("calls forwarded")} `);
259
+ // Polished summary — real P99, real audit hint, premium panel.
260
+ const sanitized = steps.filter((s) => s.event.decision === DECISION.SANITIZE).length;
261
+ write(` ${green(bold(String(allowed)))} ${dim("allowed")} `);
262
+ if (sanitized) write(`${blue(bold(String(sanitized)))} ${dim("sanitized")} `);
245
263
  write(`${red(bold(String(denied)))} ${dim("blocked")} `);
246
264
  write(`${amber(bold(String(held)))} ${dim("held for a human")} `);
247
- write(`${dim(`P99 ${p.p99}ms over ${p.samples} decisions`)}\n\n`);
248
- write(` ${bold("Cirvix did not disable the agent. It made the dangerous half controllable.")}\n\n`);
265
+ write(`${dim(`P99 ${p.p99}ms`)}${dim(` over ${p.samples} decisions · `)}${dim(`P50 ${p.p50}ms · P95 ${p.p95}ms`)}\n\n`);
266
+
267
+ // Summary panel — spec: CIRVIX GOVERNANCE boxed, P50/P95/P99, audit chain intact
268
+ const summaryLines = [
269
+ `${bold("CIRVIX GOVERNANCE")}`,
270
+ ``,
271
+ `${`${allowed} allowed`.padEnd(14)} ${`${sanitized} sanitized`.padEnd(14)} ${`${denied} blocked`.padEnd(14)} ${`${held} awaiting approval`}`,
272
+ ``,
273
+ `${`P50 ${p.p50}ms`.padEnd(14)} ${`P95 ${p.p95}ms`.padEnd(14)} ${`P99 ${p.p99}ms`}`,
274
+ ``,
275
+ `${green("✓")} ${dim(`${steps.length} records verified`)}`,
276
+ `${green("✓")} ${bold("AUDIT CHAIN INTACT")}`,
277
+ ];
278
+ // Use header box per spec: ╭─ CIRVIX GOVERNANCE ─
279
+ const W = 62;
280
+ const top = ` ${dim(`╭─ CIRVIX GOVERNANCE ${"─".repeat(Math.max(0, W - 20))}╮`)}`;
281
+ const bottom = ` ${dim(`╰${"─".repeat(W)}╯`)}`;
282
+ write(top + "\n");
283
+ for (const l of summaryLines) {
284
+ // Inside panel, pad to width, no side borders for genuine CLI feel per spec — but keep subtle
285
+ write(` ${l}\n`);
286
+ }
287
+ write(bottom + "\n\n");
288
+ write(` ${bold("CIRVIX did not disable the agent.")}\n`);
289
+ write(` ${dim("It controlled the dangerous actions.")}\n\n`);
249
290
  write(` ${dim("Every decision above is in the audit chain:")} ${blue("cirvix logs")}\n`);
250
291
  write(` ${dim("Ask why any one of them happened:")} ${blue("cirvix logs --tree <request-id>")}\n\n`);
251
292
 
252
293
  return { result, output: "" };
253
294
  }
254
295
 
255
- /* -------------------------------------------------------------------------- */
256
- /* Rendering */
257
- /* -------------------------------------------------------------------------- */
258
-
259
- /**
260
- * The intercept box.
261
- *
262
- * Fixed inner width so the borders line up regardless of content length; values
263
- * are truncated rather than allowed to break the frame, because a box with one
264
- * ragged edge reads as a rendering bug and undermines everything inside it.
265
- */
266
- function interceptBox(event) {
267
- const W = 58;
268
- const rows = [
269
- ["Agent", event.agent],
270
- ["Tool", event.tool],
271
- ["Target", event.resource || event.destination || "—"],
272
- ["Risk", String(event.risk).toUpperCase()],
273
- ["Decision", "BLOCKED"],
274
- ["Policy", event.policy ?? "default-deny"],
275
- ["Latency", `${event.latency_ms}ms`],
276
- ];
277
-
278
- const pad = (text) => {
279
- const s = String(text);
280
- return s.length > W ? s.slice(0, W - 1) + "…" : s.padEnd(W);
281
- };
282
-
283
- const lines = [
284
- ` ${red("╔" + "═".repeat(W + 2) + "╗")}`,
285
- ` ${red("║")} ${bold(pad("CIRVIX SECURITY INTERCEPT"))} ${red("║")}`,
286
- ` ${red("╠" + "═".repeat(W + 2) + "╣")}`,
287
- ...rows.map(([k, v]) => {
288
- const body = `${k}:`.padEnd(11) + v;
289
- const painted = k === "Risk" || k === "Decision" ? red(pad(body)) : pad(body);
290
- return ` ${red("║")} ${painted} ${red("║")}`;
291
- }),
292
- ` ${red("╚" + "═".repeat(W + 2) + "╝")}`,
293
- ];
294
-
295
- // The reason sits outside the box: it is the part that varies in length, and
296
- // it is what the agent itself receives as a readable tool result.
297
- const reason = event.reason ? `\n ${dim(event.reason)}\n` : "";
298
- return lines.join("\n") + "\n" + reason;
299
- }
300
-
296
+ /* Legacy oneLine kept for tests that import it indirectly — now delegates to ui. */
301
297
  function oneLine(event) {
302
- const tone =
303
- { allow: green, sanitize: blue, require_approval: amber, deny: red, audit_only: dim }[event.decision] ?? dim;
304
- const riskTone = { low: dim, medium: blue, high: amber, critical: red }[event.risk] ?? dim;
305
-
306
- return (
307
- ` ${tone(String(event.decision).toUpperCase().replace(/_/g, " ").padEnd(17))}` +
308
- `${riskTone(String(event.risk).toUpperCase().padEnd(9))}` +
309
- `${String(event.tool).padEnd(20)}` +
310
- `${dim(String(event.resource || event.command || "").slice(-38).padEnd(38))} ` +
311
- `${dim(`${event.latency_ms}ms`)}\n`
312
- );
298
+ return renderDecision(event) + "\n";
313
299
  }
314
300
 
315
301
  export { POISONED_PAGE };