@cirvix_ai/agent-control 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -17
- package/bin/cirvix.mjs +488 -40
- package/bin/escape-benchmark.mjs +67 -0
- package/package.json +36 -16
- package/src/adapters/base.mjs +150 -0
- package/src/adapters/claude-code.mjs +161 -0
- package/src/adapters/cline.mjs +107 -0
- package/src/adapters/codex.mjs +104 -0
- package/src/adapters/cursor.mjs +104 -0
- package/src/adapters/frameworks.mjs +110 -0
- package/src/adapters/gemini-cli.mjs +104 -0
- package/src/adapters/generic-mcp.mjs +101 -0
- package/src/adapters/index.mjs +209 -0
- package/src/adapters/roo-code.mjs +106 -0
- package/src/adapters/vscode.mjs +104 -0
- package/src/adapters/windsurf.mjs +107 -0
- package/src/commands/demo.mjs +56 -70
- package/src/commands/doctor.mjs +235 -0
- package/src/commands/init.mjs +292 -30
- package/src/commands/interactive.mjs +690 -0
- package/src/commands/kill.mjs +74 -0
- package/src/commands/login.mjs +227 -0
- package/src/commands/passport.mjs +149 -0
- package/src/commands/policy.mjs +10 -6
- package/src/commands/protect.mjs +293 -0
- package/src/commands/prove.mjs +209 -0
- package/src/commands/redteam.mjs +51 -0
- package/src/commands/scan.mjs +6 -4
- package/src/commands/shadow.mjs +62 -0
- package/src/commands/simulate.mjs +96 -0
- package/src/commands/status.mjs +121 -36
- package/src/commands/upgrade.mjs +17 -9
- package/src/commands/welcome.mjs +105 -0
- package/src/core/authority.mjs +909 -0
- package/src/core/baseline.mjs +97 -0
- package/src/core/config-store.mjs +280 -0
- package/src/core/cost.mjs +0 -0
- package/src/core/detect.mjs +4 -33
- package/src/core/entitlements.mjs +6 -0
- package/src/core/escape-benchmark.mjs +597 -0
- package/src/core/evidence.mjs +212 -0
- package/src/core/format.mjs +27 -0
- package/src/core/gateway.mjs +15 -211
- package/src/core/graph.mjs +270 -0
- package/src/core/guard.mjs +118 -4
- package/src/core/intent.mjs +166 -0
- package/src/core/journal.mjs +131 -40
- package/src/core/kill-switch.mjs +122 -0
- package/src/core/notices.mjs +22 -2
- package/src/core/packs.mjs +193 -0
- package/src/core/passport.mjs +555 -0
- package/src/core/pipeline.mjs +148 -6
- package/src/core/prompts.mjs +51 -0
- package/src/core/proof.mjs +440 -0
- package/src/core/redteam/index.mjs +185 -0
- package/src/core/referral.mjs +187 -0
- package/src/core/sandbox.mjs +139 -0
- package/src/core/session.mjs +172 -0
- package/src/core/shadow.mjs +95 -0
- package/src/core/trifecta.mjs +321 -0
- package/src/core/ui/controller.mjs +192 -0
- package/src/core/ui/decisions.mjs +55 -0
- package/src/core/ui/index.mjs +49 -0
- package/src/core/ui/intercept.mjs +103 -0
- package/src/core/ui/live.mjs +51 -0
- package/src/core/ui/primitives.mjs +123 -0
- package/src/core/ui/theme.mjs +92 -0
- package/src/core/verified.mjs +108 -0
- package/src/core/windows.mjs +270 -0
- package/src/index.mjs +25 -0
|
@@ -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
|
+
}
|
package/src/commands/demo.mjs
CHANGED
|
@@ -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(
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|