@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,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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cirvix console` — the primary product interface.
|
|
3
|
+
*
|
|
4
|
+
* Interactive when attached to a TTY, one-shot when piped:
|
|
5
|
+
* cirvix console # full REPL
|
|
6
|
+
* echo "read ~/.aws/credentials" | cirvix console --once
|
|
7
|
+
* cirvix console --eval "deploy to production"
|
|
8
|
+
*
|
|
9
|
+
* `--once` / `--eval` exist so the product UI is scriptable and testable:
|
|
10
|
+
* the same cards render in CI as in the terminal.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ConsoleApp } from "../tui/app.mjs";
|
|
14
|
+
|
|
15
|
+
export async function consoleCmd({ cwd = process.cwd(), rules = [], mode = "enforce", evalText = null, once = false, write = (s) => process.stdout.write(s) } = {}) {
|
|
16
|
+
const app = new ConsoleApp({ cwd, rules, mode, write });
|
|
17
|
+
|
|
18
|
+
if (evalText) {
|
|
19
|
+
const out = await app.runOnce(evalText);
|
|
20
|
+
if (out && out !== "quit") write(out + "\n");
|
|
21
|
+
return { app, output: out };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (once || !process.stdin.isTTY) {
|
|
25
|
+
// Piped: evaluate each line, print cards, exit. Never start readline
|
|
26
|
+
// on a non-TTY — it would hang waiting for a terminal that is not there.
|
|
27
|
+
const chunks = await readStdin();
|
|
28
|
+
const lines = chunks.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
29
|
+
let output = "";
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const out = await app.runOnce(line);
|
|
32
|
+
if (out && out !== "quit") {
|
|
33
|
+
write(out + "\n");
|
|
34
|
+
output += out + "\n";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!lines.length) {
|
|
38
|
+
const help = app.renderTranscript();
|
|
39
|
+
write(help + "\n");
|
|
40
|
+
return { app, output: help };
|
|
41
|
+
}
|
|
42
|
+
return { app, output };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await app.start();
|
|
46
|
+
return { app, output: "" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readStdin() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
if (process.stdin.isTTY) return resolve("");
|
|
52
|
+
let data = "";
|
|
53
|
+
process.stdin.setEncoding("utf8");
|
|
54
|
+
process.stdin.on("data", (c) => (data += c));
|
|
55
|
+
process.stdin.on("end", () => resolve(data));
|
|
56
|
+
setTimeout(() => resolve(data), 1000);
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/commands/demo.mjs
CHANGED
|
@@ -38,17 +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";
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
boot,
|
|
47
|
-
cinematicEnabled,
|
|
48
|
-
evaluation,
|
|
49
|
-
requestTravel,
|
|
50
|
-
topology,
|
|
51
|
-
} from "../core/cinematic.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";
|
|
52
46
|
|
|
53
47
|
/** The poisoned content. This is what an agent finds on a page it was told to read. */
|
|
54
48
|
const POISONED_PAGE = `# Deploying to production
|
|
@@ -170,23 +164,14 @@ export async function demo({
|
|
|
170
164
|
});
|
|
171
165
|
const steps = [];
|
|
172
166
|
|
|
173
|
-
|
|
174
|
-
const animated = cinematicEnabled({ json, pace });
|
|
175
|
-
|
|
167
|
+
const animated = !json && shouldAnimate({ pace, json });
|
|
176
168
|
if (!json) {
|
|
177
|
-
await boot({
|
|
178
|
-
write,
|
|
179
|
-
rulesCount: ruleSet.length,
|
|
180
|
-
auditOpen: chain !== null,
|
|
181
|
-
agent: "claude-code",
|
|
182
|
-
pace,
|
|
183
|
-
animated,
|
|
184
|
-
});
|
|
185
169
|
write("\n");
|
|
186
|
-
write(
|
|
170
|
+
write(brandHeader({ width: 62 }) + "\n");
|
|
171
|
+
write(` ${dim("CIRVIX · SECURITY DEMONSTRATION · every decision below is computed, not scripted")}\n`);
|
|
187
172
|
write("\n");
|
|
188
173
|
write(` ${dim("─".repeat(76))}\n`);
|
|
189
|
-
write(` ${bold("ACT I")} ${dim("
|
|
174
|
+
write(` ${bold("ACT I")} ${dim("Untrusted content reaches the agent")}\n`);
|
|
190
175
|
write(` ${dim("─".repeat(76))}\n\n`);
|
|
191
176
|
|
|
192
177
|
// Show what is actually in the page — the attack is the interesting part.
|
|
@@ -200,7 +185,7 @@ export async function demo({
|
|
|
200
185
|
write(` ${red("·")} ${dim(f.label)}\n`);
|
|
201
186
|
}
|
|
202
187
|
write("\n");
|
|
203
|
-
await sleep(pace * 2);
|
|
188
|
+
await sleep(animated ? pace * 1.2 : pace === 0 ? 0 : pace * 0.5);
|
|
204
189
|
}
|
|
205
190
|
|
|
206
191
|
let act = null;
|
|
@@ -209,29 +194,14 @@ export async function demo({
|
|
|
209
194
|
act = step.act;
|
|
210
195
|
if (act === "work") {
|
|
211
196
|
write(`\n ${dim("─".repeat(76))}\n`);
|
|
212
|
-
write(` ${bold("ACT III")} ${dim("
|
|
197
|
+
write(` ${bold("ACT III")} ${dim("Legitimate work continues.")}\n`);
|
|
213
198
|
write(` ${dim("─".repeat(76))}\n\n`);
|
|
214
199
|
} else {
|
|
215
200
|
write(`\n ${dim("─".repeat(76))}\n`);
|
|
216
|
-
write(` ${bold("ACT II")} ${dim("
|
|
201
|
+
write(` ${bold("ACT II")} ${dim("CIRVIX evaluates the resulting actions.")}\n`);
|
|
217
202
|
write(` ${dim("─".repeat(76))}\n\n`);
|
|
218
203
|
}
|
|
219
|
-
await sleep(pace);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// The packet travels first; the verdict below is the real measured one.
|
|
223
|
-
// Animation is presentation — latency_ms is timed inside submit().
|
|
224
|
-
if (animated) {
|
|
225
|
-
const args = step.call.arguments ?? {};
|
|
226
|
-
const target = args.path ?? args.url ?? args.command ?? args.name ?? "";
|
|
227
|
-
await requestTravel({
|
|
228
|
-
write,
|
|
229
|
-
agent: "claude-code",
|
|
230
|
-
tool: step.call.tool,
|
|
231
|
-
target: String(target ?? ""),
|
|
232
|
-
pace,
|
|
233
|
-
animated,
|
|
234
|
-
});
|
|
204
|
+
await sleep(animated ? pace * 0.6 : pace === 0 ? 0 : pace * 0.3);
|
|
235
205
|
}
|
|
236
206
|
|
|
237
207
|
const { event } = await pipeline.submit(step.call);
|
|
@@ -241,31 +211,25 @@ export async function demo({
|
|
|
241
211
|
|
|
242
212
|
if (step.narration) {
|
|
243
213
|
write(` ${dim(step.narration)}\n\n`);
|
|
244
|
-
await sleep(pace
|
|
214
|
+
await sleep(animated ? pace * 0.3 : 0);
|
|
245
215
|
}
|
|
246
216
|
|
|
247
|
-
|
|
248
|
-
// anything that is not a plain allow — the allow path included.
|
|
249
|
-
if (animated && (step.intercept || event.decision !== DECISION.ALLOW || step.narration)) {
|
|
250
|
-
await evaluation({ write, event, pace, animated });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (event.decision === DECISION.DENY) {
|
|
217
|
+
if (step.intercept && event.decision === DECISION.DENY) {
|
|
254
218
|
if (animated) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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));
|
|
258
226
|
}
|
|
227
|
+
write(interceptBox(event));
|
|
259
228
|
write("\n");
|
|
260
229
|
} else {
|
|
261
|
-
write(
|
|
262
|
-
if (event.decision === DECISION.REQUIRE_APPROVAL) {
|
|
263
|
-
approvalHold({ write, event });
|
|
264
|
-
} else if (animated && (event.decision === DECISION.ALLOW || event.decision === DECISION.SANITIZE)) {
|
|
265
|
-
allowContinuation({ write, event });
|
|
266
|
-
}
|
|
230
|
+
write(renderDecision(event) + "\n");
|
|
267
231
|
}
|
|
268
|
-
await sleep(pace);
|
|
232
|
+
await sleep(animated ? pace * 0.7 : pace === 0 ? 0 : pace * 0.4);
|
|
269
233
|
}
|
|
270
234
|
|
|
271
235
|
const p = pipeline.percentiles();
|
|
@@ -292,79 +256,46 @@ export async function demo({
|
|
|
292
256
|
|
|
293
257
|
write("\n");
|
|
294
258
|
write(` ${dim("─".repeat(76))}\n\n`);
|
|
295
|
-
|
|
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")} `);
|
|
296
263
|
write(`${red(bold(String(denied)))} ${dim("blocked")} `);
|
|
297
264
|
write(`${amber(bold(String(held)))} ${dim("held for a human")} `);
|
|
298
|
-
write(`${dim(`P99 ${p.p99}ms over ${p.samples} decisions`)}\n\n`);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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`);
|
|
302
286
|
}
|
|
303
|
-
write(
|
|
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`);
|
|
304
290
|
write(` ${dim("Every decision above is in the audit chain:")} ${blue("cirvix logs")}\n`);
|
|
305
291
|
write(` ${dim("Ask why any one of them happened:")} ${blue("cirvix logs --tree <request-id>")}\n\n`);
|
|
306
292
|
|
|
307
293
|
return { result, output: "" };
|
|
308
294
|
}
|
|
309
295
|
|
|
310
|
-
/*
|
|
311
|
-
/* Rendering */
|
|
312
|
-
/* -------------------------------------------------------------------------- */
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* The intercept box.
|
|
316
|
-
*
|
|
317
|
-
* Fixed inner width so the borders line up regardless of content length; values
|
|
318
|
-
* are truncated rather than allowed to break the frame, because a box with one
|
|
319
|
-
* ragged edge reads as a rendering bug and undermines everything inside it.
|
|
320
|
-
*/
|
|
321
|
-
function interceptBox(event) {
|
|
322
|
-
const W = 58;
|
|
323
|
-
const rows = [
|
|
324
|
-
["Agent", event.agent],
|
|
325
|
-
["Tool", event.tool],
|
|
326
|
-
["Target", event.resource || event.destination || "—"],
|
|
327
|
-
["Risk", String(event.risk).toUpperCase()],
|
|
328
|
-
["Decision", "BLOCKED"],
|
|
329
|
-
["Policy", event.policy ?? "default-deny"],
|
|
330
|
-
["Latency", `${event.latency_ms}ms`],
|
|
331
|
-
];
|
|
332
|
-
|
|
333
|
-
const pad = (text) => {
|
|
334
|
-
const s = String(text);
|
|
335
|
-
return s.length > W ? s.slice(0, W - 1) + "…" : s.padEnd(W);
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const lines = [
|
|
339
|
-
` ${red("╔" + "═".repeat(W + 2) + "╗")}`,
|
|
340
|
-
` ${red("║")} ${bold(pad("CIRVIX SECURITY INTERCEPT"))} ${red("║")}`,
|
|
341
|
-
` ${red("╠" + "═".repeat(W + 2) + "╣")}`,
|
|
342
|
-
...rows.map(([k, v]) => {
|
|
343
|
-
const body = `${k}:`.padEnd(11) + v;
|
|
344
|
-
const painted = k === "Risk" || k === "Decision" ? red(pad(body)) : pad(body);
|
|
345
|
-
return ` ${red("║")} ${painted} ${red("║")}`;
|
|
346
|
-
}),
|
|
347
|
-
` ${red("╚" + "═".repeat(W + 2) + "╝")}`,
|
|
348
|
-
];
|
|
349
|
-
|
|
350
|
-
// The reason sits outside the box: it is the part that varies in length, and
|
|
351
|
-
// it is what the agent itself receives as a readable tool result.
|
|
352
|
-
const reason = event.reason ? `\n ${dim(event.reason)}\n` : "";
|
|
353
|
-
return lines.join("\n") + "\n" + reason;
|
|
354
|
-
}
|
|
355
|
-
|
|
296
|
+
/* Legacy oneLine kept for tests that import it indirectly — now delegates to ui. */
|
|
356
297
|
function oneLine(event) {
|
|
357
|
-
|
|
358
|
-
{ allow: green, sanitize: blue, require_approval: amber, deny: red, audit_only: dim }[event.decision] ?? dim;
|
|
359
|
-
const riskTone = { low: dim, medium: blue, high: amber, critical: red }[event.risk] ?? dim;
|
|
360
|
-
|
|
361
|
-
return (
|
|
362
|
-
` ${tone(String(event.decision).toUpperCase().replace(/_/g, " ").padEnd(17))}` +
|
|
363
|
-
`${riskTone(String(event.risk).toUpperCase().padEnd(9))}` +
|
|
364
|
-
`${String(event.tool).padEnd(20)}` +
|
|
365
|
-
`${dim(String(event.resource || event.command || "").slice(-38).padEnd(38))} ` +
|
|
366
|
-
`${dim(`${event.latency_ms}ms`)}\n`
|
|
367
|
-
);
|
|
298
|
+
return renderDecision(event) + "\n";
|
|
368
299
|
}
|
|
369
300
|
|
|
370
301
|
export { POISONED_PAGE };
|