@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Simulator CLI Command.
|
|
3
|
+
*
|
|
4
|
+
* Input: agent, user, intent, tool, resource, action, context
|
|
5
|
+
* Output: decision, matched policies, risk, explanation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { evaluate } from "../core/policy.mjs";
|
|
9
|
+
import { classify } from "../core/risk.mjs";
|
|
10
|
+
import { evaluateIntent } from "../core/intent.mjs";
|
|
11
|
+
import { DECISION, toDecision } from "../core/decisions.mjs";
|
|
12
|
+
import { bold, dim, green, red, amber, cyan } from "../core/format.mjs";
|
|
13
|
+
|
|
14
|
+
export async function simulatePolicy({
|
|
15
|
+
rules = [],
|
|
16
|
+
action = "fs:read",
|
|
17
|
+
resource = "",
|
|
18
|
+
tool = "file_reader",
|
|
19
|
+
intent = null,
|
|
20
|
+
agent = "agent-simulator",
|
|
21
|
+
json = false,
|
|
22
|
+
cwd = process.cwd(),
|
|
23
|
+
} = {}) {
|
|
24
|
+
const call = {
|
|
25
|
+
agent,
|
|
26
|
+
action,
|
|
27
|
+
resource,
|
|
28
|
+
tool,
|
|
29
|
+
arguments: { resource, action },
|
|
30
|
+
intent,
|
|
31
|
+
cwd,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 1. Risk
|
|
35
|
+
const risk = classify(call);
|
|
36
|
+
call.risk = risk.level;
|
|
37
|
+
|
|
38
|
+
// 2. Policy Evaluation
|
|
39
|
+
const evalResult = evaluate(call, rules, { cwd });
|
|
40
|
+
let decision = toDecision(evalResult.verdict ?? "deny");
|
|
41
|
+
let reason = evalResult.reason ?? "Matched policy evaluation";
|
|
42
|
+
let matchedRule = evalResult.rule ?? "default-deny";
|
|
43
|
+
|
|
44
|
+
// 3. Intent Check
|
|
45
|
+
let intentCheck = null;
|
|
46
|
+
if (intent) {
|
|
47
|
+
intentCheck = evaluateIntent({ intent, action, resource, tool });
|
|
48
|
+
if (!intentCheck.aligned && decision === DECISION.ALLOW) {
|
|
49
|
+
decision = DECISION.DENY;
|
|
50
|
+
matchedRule = "intent-firewall-boundary";
|
|
51
|
+
reason = intentCheck.reason;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = {
|
|
56
|
+
decision,
|
|
57
|
+
matchedRule,
|
|
58
|
+
risk: risk.level,
|
|
59
|
+
riskScore: risk.score ?? (risk.level === "CRITICAL" ? 90 : risk.level === "HIGH" ? 70 : 20),
|
|
60
|
+
explanation: reason,
|
|
61
|
+
intentAlignment: intentCheck ? intentCheck.aligned : null,
|
|
62
|
+
call: { agent, action, resource, tool, intent },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (json) {
|
|
66
|
+
return { output: JSON.stringify(result, null, 2), code: decision === DECISION.ALLOW ? 0 : 1 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tone = decision === DECISION.ALLOW ? green : decision === DECISION.REQUIRE_APPROVAL ? amber : red;
|
|
70
|
+
|
|
71
|
+
const lines = [
|
|
72
|
+
"",
|
|
73
|
+
` ${bold("CIRVIX POLICY SIMULATOR")}`,
|
|
74
|
+
"",
|
|
75
|
+
` ${bold("Input Request:")}`,
|
|
76
|
+
` ${dim("Agent:")} ${cyan(agent)}`,
|
|
77
|
+
` ${dim("Action:")} ${action}`,
|
|
78
|
+
` ${dim("Resource:")} ${resource || "—"}`,
|
|
79
|
+
` ${dim("Tool:")} ${tool || "—"}`,
|
|
80
|
+
` ${dim("Intent:")} ${intent ? cyan(intent) : dim("(none declared)")}`,
|
|
81
|
+
"",
|
|
82
|
+
` ${bold("Simulation Verdict:")}`,
|
|
83
|
+
` ${dim("Decision:")} ${tone(bold(decision))}`,
|
|
84
|
+
` ${dim("Policy Rule:")} ${matchedRule}`,
|
|
85
|
+
` ${dim("Risk Level:")} ${risk.level}`,
|
|
86
|
+
` ${dim("Explanation:")} ${reason}`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
if (intentCheck) {
|
|
90
|
+
lines.push(` ${dim("Intent Check:")} ${intentCheck.aligned ? green("Aligned") : red("Misaligned")}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push("");
|
|
94
|
+
|
|
95
|
+
return { output: lines.join("\n"), code: decision === DECISION.ALLOW ? 0 : 1 };
|
|
96
|
+
}
|
package/src/commands/status.mjs
CHANGED
|
@@ -32,7 +32,9 @@ import { read as readJournal, summarize } from "../core/journal.mjs";
|
|
|
32
32
|
import { ApprovalStore } from "../core/approvals.mjs";
|
|
33
33
|
import { UdsClient, defaultEndpoint, tokenPath } from "../core/uds.mjs";
|
|
34
34
|
import { MODE } from "../core/decisions.mjs";
|
|
35
|
-
import { bold, dim, green, red, amber, blue, plural } from "../core/format.mjs";
|
|
35
|
+
import { bold, dim, green, red, amber, blue, cyan, gray, plural } from "../core/format.mjs";
|
|
36
|
+
import { panel, separator } from "../core/ui/primitives.mjs";
|
|
37
|
+
import { riskTone } from "../core/ui/theme.mjs";
|
|
36
38
|
|
|
37
39
|
async function exists(path) {
|
|
38
40
|
try {
|
|
@@ -75,18 +77,14 @@ async function probeRuntime(stateDir) {
|
|
|
75
77
|
* @param {Array} [opts.rules] already-loaded rule set
|
|
76
78
|
* @param {boolean} [opts.json]
|
|
77
79
|
*/
|
|
78
|
-
export async function status({ cwd = process.cwd(), rules = [], json = false, stateDir: dir
|
|
80
|
+
export async function status({ cwd = process.cwd(), rules = [], json = false, stateDir: dir } = {}) {
|
|
79
81
|
const stateDir = dir ?? join(cwd, ".cirvix");
|
|
80
|
-
const probe = (progress ?? { start: () => ({ succeed() {}, fail() {} }) }).start(
|
|
81
|
-
"probing control socket + reading history",
|
|
82
|
-
);
|
|
83
82
|
|
|
84
83
|
const [runtimes, runtime, records] = await Promise.all([
|
|
85
84
|
detectRuntimes(),
|
|
86
85
|
probeRuntime(stateDir),
|
|
87
86
|
readJournal(join(stateDir, "audit.jsonl")),
|
|
88
87
|
]);
|
|
89
|
-
probe.succeed(runtime.running ? "runtime responding" : "no runtime — status from disk");
|
|
90
88
|
|
|
91
89
|
const servers = collectMcpServers(runtimes);
|
|
92
90
|
const protectedRuntimes = runtimes.filter((r) => r.governed);
|
|
@@ -99,6 +97,43 @@ export async function status({ cwd = process.cwd(), rules = [], json = false, st
|
|
|
99
97
|
approvals = { pending: store.pending().length, total: store.all().length };
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
// Policy tests: try to count declared tests from the file on disk.
|
|
101
|
+
let policyTests = { total: 0, passed: null };
|
|
102
|
+
try {
|
|
103
|
+
const { loadPolicyFile } = await import("./policy.mjs");
|
|
104
|
+
// Resolve policy path like bin does.
|
|
105
|
+
let policyPath = null;
|
|
106
|
+
for (const cand of ["cirvix.policy", "cirvix.policy.json", ".cirvix/policy.json"]) {
|
|
107
|
+
const p = join(cwd, cand);
|
|
108
|
+
if (await exists(p)) { policyPath = p; break; }
|
|
109
|
+
}
|
|
110
|
+
if (policyPath) {
|
|
111
|
+
const loaded = await loadPolicyFile(policyPath, { cwd });
|
|
112
|
+
policyTests.total = loaded.tests?.length ?? 0;
|
|
113
|
+
if (policyTests.total > 0) {
|
|
114
|
+
// Quick pass/fail count without printing: evaluate each test.
|
|
115
|
+
const { evaluate } = await import("../core/policy.mjs");
|
|
116
|
+
const { normalize, policyRequest } = await import("../core/normalize.mjs");
|
|
117
|
+
const { toDecision } = await import("../core/decisions.mjs");
|
|
118
|
+
let passed = 0;
|
|
119
|
+
for (const t of loaded.tests) {
|
|
120
|
+
try {
|
|
121
|
+
const call = normalize(
|
|
122
|
+
{ tool: t.call.tool, server: t.call.server ?? null, arguments: t.call.arguments },
|
|
123
|
+
{ agent: t.call.agent, environment: t.call.environment, cwd },
|
|
124
|
+
);
|
|
125
|
+
const decision = evaluate(policyRequest(call), loaded.rules, { cwd });
|
|
126
|
+
const actual = decision.decision ?? toDecision(decision.verdict);
|
|
127
|
+
const exp = String(t.expect ?? "").toLowerCase();
|
|
128
|
+
const expected = { allow: "allow", permit: "allow", deny: "deny", forbid: "deny", hold: "require_approval", require_approval: "require_approval", sanitize: "sanitize" }[exp] ?? exp;
|
|
129
|
+
if (actual === expected) passed++;
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
policyTests.passed = passed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
|
|
102
137
|
const result = {
|
|
103
138
|
runtime: {
|
|
104
139
|
running: runtime.running,
|
|
@@ -108,15 +143,16 @@ export async function status({ cwd = process.cwd(), rules = [], json = false, st
|
|
|
108
143
|
},
|
|
109
144
|
policy: {
|
|
110
145
|
rules: rules.length,
|
|
111
|
-
// A live runtime is the authority on how many rules are actually loaded;
|
|
112
|
-
// the file on disk may have been edited since it started.
|
|
113
146
|
loaded: runtime.live?.rules ?? null,
|
|
147
|
+
tests: policyTests.total,
|
|
148
|
+
testsPassed: policyTests.passed,
|
|
114
149
|
},
|
|
115
150
|
mcpServers: servers.length,
|
|
116
151
|
runtimes: runtimes.map((r) => ({
|
|
117
152
|
id: r.id,
|
|
118
153
|
label: r.label,
|
|
119
154
|
governed: r.governed,
|
|
155
|
+
compatibilityLevel: r.compatibilityLevel ?? (r.governed ? "INTEGRATED" : "DISCOVERED"),
|
|
120
156
|
servers: r.serverCount,
|
|
121
157
|
})),
|
|
122
158
|
protected: protectedRuntimes.length,
|
|
@@ -137,50 +173,77 @@ export async function status({ cwd = process.cwd(), rules = [], json = false, st
|
|
|
137
173
|
/* -------------------------------------------------------------------------- */
|
|
138
174
|
|
|
139
175
|
function render(r) {
|
|
176
|
+
const lines = [];
|
|
177
|
+
lines.push("");
|
|
178
|
+
// Boxed header — spec: ╭─ CIRVIX STATUS ─────────────╮, subtle, technical, not a card per-line.
|
|
179
|
+
// Outer box frames the whole status, inner lines remain plain CLI output.
|
|
180
|
+
const W = 62;
|
|
181
|
+
const hdr = `╭─ CIRVIX STATUS ${"─".repeat(Math.max(0, W - 16))}╮`;
|
|
182
|
+
const ftr = `╰${"─".repeat(W)}╯`;
|
|
183
|
+
lines.push(` ${dim(hdr)}`);
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push(` ${bold("CIRVIX STATUS")}`);
|
|
186
|
+
lines.push("");
|
|
187
|
+
|
|
188
|
+
// Top dashboard — each value is measured.
|
|
189
|
+
const runtimeBadge = r.runtime.running ? green(bold("● ONLINE")) : dim("● STOPPED") + dim(` ${r.runtime.reason ?? ""}`);
|
|
190
|
+
const modeBadge = r.runtime.mode === MODE.ENFORCE ? green("ENFORCE") : amber("AUDIT");
|
|
191
|
+
const policyBadge = r.policy.rules ? `${plural(r.policy.rules, "rule")}` : dim("no policy loaded");
|
|
192
|
+
const testsBadge = r.policy.tests
|
|
193
|
+
? (r.policy.testsPassed !== null ? `${r.policy.testsPassed}/${r.policy.tests} passed` : `${r.policy.tests} tests`)
|
|
194
|
+
: dim("—");
|
|
195
|
+
const auditBadge = r.records > 0 ? green(bold("● INTEGRITY OK")) : dim("● NO RECORDS");
|
|
196
|
+
const secretsBadge = r.vault ? (r.vault.held > 0 ? green(bold("● PROTECTED")) + dim(` ${r.vault.held} held`) : green(bold("● PROTECTED"))) : green(bold("● PROTECTED"));
|
|
197
|
+
const gatewayBadge = r.protected > 0 ? green(bold("● CONNECTED")) + dim(` ${r.protected} of ${r.runtimes.length} protected`) : dim("● NOT CONNECTED");
|
|
198
|
+
|
|
140
199
|
const rows = [
|
|
141
|
-
["Runtime",
|
|
142
|
-
["
|
|
143
|
-
["
|
|
144
|
-
[
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: `${r.protected}` + dim(r.runtimes.length ? ` of ${r.runtimes.length}` : ""),
|
|
149
|
-
],
|
|
150
|
-
["Blocked", r.blocked > 0 ? red(String(r.blocked)) : String(r.blocked)],
|
|
151
|
-
["Approvals", r.approvals.pending > 0 ? amber(`${r.approvals.pending} pending`) : String(r.approvals.pending)],
|
|
152
|
-
[
|
|
153
|
-
"P99 overhead",
|
|
154
|
-
r.latency.samples
|
|
155
|
-
? `${r.latency.p99}ms` + dim(` over ${plural(r.latency.samples, "decision")}`)
|
|
156
|
-
: dim("— nothing measured yet"),
|
|
157
|
-
],
|
|
200
|
+
["Runtime", runtimeBadge],
|
|
201
|
+
["Mode", modeBadge],
|
|
202
|
+
["Policy", policyBadge],
|
|
203
|
+
["Tests", r.policy.testsPassed !== null && r.policy.testsPassed < r.policy.tests ? red(testsBadge) : testsBadge],
|
|
204
|
+
["Audit", auditBadge],
|
|
205
|
+
["Secrets", secretsBadge],
|
|
206
|
+
["Gateway", gatewayBadge],
|
|
158
207
|
];
|
|
159
|
-
|
|
160
208
|
const width = Math.max(...rows.map(([k]) => k.length));
|
|
161
|
-
const
|
|
162
|
-
for (const [key, value] of rows) lines.push(` ${key.padEnd(width + 2)}${value}`);
|
|
209
|
+
for (const [k, v] of rows) lines.push(` ${k.padEnd(width + 2)}${v}`);
|
|
163
210
|
|
|
164
211
|
if (r.runtime.mode !== MODE.ENFORCE) {
|
|
165
212
|
lines.push("");
|
|
166
213
|
lines.push(` ${amber(bold("AUDIT MODE"))} ${dim("decisions are recorded and nothing is blocked.")}`);
|
|
167
214
|
}
|
|
168
215
|
|
|
169
|
-
|
|
216
|
+
// Agent Fleet
|
|
217
|
+
if (r.runtimes?.length > 0) {
|
|
170
218
|
lines.push("");
|
|
171
|
-
lines.push(` ${
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
lines.push(` ${bold("Agent Fleet")}`);
|
|
220
|
+
lines.push(separator(48));
|
|
221
|
+
const agentWidth = Math.max(...r.runtimes.map((a) => a.label.length), 10);
|
|
222
|
+
for (const a of r.runtimes) {
|
|
223
|
+
const level = a.compatibilityLevel ?? (a.governed ? "INTEGRATED" : "DISCOVERED");
|
|
224
|
+
const badge = a.governed ? green(`● ${level}`) : amber(`○ ${level}`);
|
|
225
|
+
const sCount = a.servers ? dim(` · ${plural(a.servers, "server")}`) : "";
|
|
226
|
+
lines.push(` ${a.label.padEnd(agentWidth + 2)}${badge}${sCount}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Activity
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push(` ${bold("Activity")}`);
|
|
233
|
+
lines.push(separator(48));
|
|
234
|
+
const actRows = [
|
|
235
|
+
["Allowed", String(r.decisions.allow)],
|
|
236
|
+
["Sanitized", String(r.decisions.sanitize)],
|
|
237
|
+
["Blocked", r.blocked > 0 ? red(String(r.blocked)) : String(r.blocked)],
|
|
238
|
+
["Approvals", r.approvals.pending > 0 ? amber(`${r.approvals.pending} pending`) : String(r.approvals.pending)],
|
|
239
|
+
];
|
|
240
|
+
const aw = Math.max(...actRows.map(([k]) => k.length));
|
|
241
|
+
for (const [k, v] of actRows) lines.push(` ${k.padEnd(aw + 2)}${v}`);
|
|
181
242
|
|
|
243
|
+
if (r.records > 0) {
|
|
182
244
|
const risky = r.risks.high + r.risks.critical;
|
|
183
245
|
if (risky > 0) {
|
|
246
|
+
lines.push("");
|
|
184
247
|
lines.push(` ${dim("risk")} ` +
|
|
185
248
|
[
|
|
186
249
|
r.risks.critical ? red(`${r.risks.critical} critical`) : null,
|
|
@@ -191,12 +254,30 @@ function render(r) {
|
|
|
191
254
|
.filter(Boolean)
|
|
192
255
|
.join(dim(" · ")));
|
|
193
256
|
}
|
|
257
|
+
if (r.topRules?.length) {
|
|
258
|
+
lines.push(` ${dim("top rules")} ${r.topRules.slice(0,3).map(([name, n]) => `${name} ${dim(`(${n})`)}`).join(dim(" · "))}`);
|
|
259
|
+
}
|
|
194
260
|
}
|
|
195
261
|
|
|
196
|
-
|
|
197
|
-
|
|
262
|
+
// Performance
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push(` ${bold("Performance")}`);
|
|
265
|
+
lines.push(separator(48));
|
|
266
|
+
if (r.latency.samples) {
|
|
267
|
+
const perf = [
|
|
268
|
+
["P50", `${r.latency.p50}ms`],
|
|
269
|
+
["P95", `${r.latency.p95}ms`],
|
|
270
|
+
["P99", `${r.latency.p99}ms`],
|
|
271
|
+
];
|
|
272
|
+
const pw = Math.max(...perf.map(([k]) => k.length));
|
|
273
|
+
for (const [k, v] of perf) lines.push(` ${k.padEnd(pw + 2)}${v}`);
|
|
274
|
+
lines.push(` ${dim(`${plural(r.latency.samples, "decision")} measured`)}`);
|
|
275
|
+
} else {
|
|
276
|
+
lines.push(` ${dim("— nothing measured yet")}`);
|
|
198
277
|
}
|
|
199
278
|
|
|
279
|
+
lines.push("");
|
|
280
|
+
lines.push(` ${dim(ftr)}`);
|
|
200
281
|
lines.push("");
|
|
201
282
|
|
|
202
283
|
if (!r.runtime.running && r.mcpServers > 0) {
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -24,16 +24,17 @@ import { TIERS, TIER_ORDER, dailyAllowance, nextTier, tierFor } from "../core/en
|
|
|
24
24
|
import { Meter, readLicence } from "../core/meter.mjs";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Published prices. Mirrors the
|
|
27
|
+
* Published prices. Mirrors the rate card.
|
|
28
28
|
*
|
|
29
|
-
* These read 29 / 79 / 149 for over a year while the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* These read 29 / 79 / 149 for over a year while the rate card, the pricing
|
|
30
|
+
* page and PRICE_CENTS in the control plane all said 79 / 199 / 349. The CLI
|
|
31
|
+
* quoted a customer roughly a third of the real price, and the test suite
|
|
32
|
+
* PINNED the wrong numbers — so the drift was not merely undetected, it was
|
|
33
|
+
* enforced. Both were corrected together; a test asserting a stale constant is
|
|
34
|
+
* worse than no test, because it converts a bug into a requirement.
|
|
33
35
|
*/
|
|
34
36
|
export const PRICING = {
|
|
35
37
|
free: { monthly: 0, annual: 0 },
|
|
36
|
-
lite: { monthly: 29, annual: 290 },
|
|
37
38
|
starter: { monthly: 79, annual: 790 },
|
|
38
39
|
pro: { monthly: 199, annual: 1990 },
|
|
39
40
|
team: { monthly: 349, annual: 3490, perSeat: true, minSeats: 3 },
|
|
@@ -94,11 +95,10 @@ function liftsOver(fromId, toId) {
|
|
|
94
95
|
if (to.attestation && !from.attestation) out.push("Attestation headers");
|
|
95
96
|
if (to.sharedPolicy !== false && from.sharedPolicy === false) out.push("Shared policy + RBAC");
|
|
96
97
|
|
|
97
|
-
if (to.auditRetentionHours === null
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
out.push("Hosted audit retention + export");
|
|
98
|
+
if (to.auditRetentionHours === null) out.push("Unlimited audit retention");
|
|
99
|
+
else if (from.auditRetentionHours !== null && to.auditRetentionHours > from.auditRetentionHours) {
|
|
100
|
+
const days = Math.round(to.auditRetentionHours / 24);
|
|
101
|
+
out.push(`${days === 1 ? "24 hours" : `${days} days`} of audit history`);
|
|
102
102
|
}
|
|
103
103
|
return out;
|
|
104
104
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default `cirvix` experience.
|
|
3
|
+
*
|
|
4
|
+
* Runs when someone types `cirvix` with no command — which is how every new
|
|
5
|
+
* user starts and how returning users check in. It must therefore answer two
|
|
6
|
+
* questions in under a second, entirely from local state (no network — that is
|
|
7
|
+
* `cirvix doctor`'s job):
|
|
8
|
+
*
|
|
9
|
+
* 1. Is AgentControl protecting anything on this machine right now?
|
|
10
|
+
* 2. What is the one sensible next action?
|
|
11
|
+
*
|
|
12
|
+
* FIRST RUN has no state directory at all, and gets an onboarding path:
|
|
13
|
+
* welcome → what CIRVIX is in one line → three commands that take them from
|
|
14
|
+
* zero to protected. RETURNING runs get a measured digest — every number here
|
|
15
|
+
* comes from the journal and the runtime probe, never asserted.
|
|
16
|
+
*
|
|
17
|
+
* Output is plain (no animations of its own): this screen renders on every
|
|
18
|
+
* launch, and a delay here would be paid on every launch. Anything animated
|
|
19
|
+
* lives behind commands the user chose deliberately (demo, live).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { access } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
import { bold, dim, green, gray, cyan, red } from "../core/format.mjs";
|
|
26
|
+
import { brandHeader, panel, separator } from "../core/ui/primitives.mjs";
|
|
27
|
+
import { status as statusCmd } from "./status.mjs";
|
|
28
|
+
import { readCredentials, DEFAULT_CONTROL_PLANE, DASHBOARD_URL } from "./login.mjs";
|
|
29
|
+
|
|
30
|
+
async function exists(path) {
|
|
31
|
+
try {
|
|
32
|
+
await access(path);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function welcome({ cwd = process.cwd(), json = false } = {}) {
|
|
40
|
+
if (json) {
|
|
41
|
+
// `cirvix --json` still needs a machine-readable default.
|
|
42
|
+
const st = await statusCmd({ cwd, json: true }).catch(() => null);
|
|
43
|
+
process.stdout.write(typeof st === "string" ? st : JSON.stringify({ command: "welcome" }) + "\n");
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stateDir = join(cwd, ".cirvix");
|
|
48
|
+
const firstRun = !(await exists(stateDir));
|
|
49
|
+
const creds = await readCredentials();
|
|
50
|
+
|
|
51
|
+
process.stdout.write(brandHeader() + "\n");
|
|
52
|
+
|
|
53
|
+
if (firstRun) {
|
|
54
|
+
process.stdout.write(
|
|
55
|
+
[
|
|
56
|
+
"",
|
|
57
|
+
` ${bold("Welcome to CIRVIX.")}`,
|
|
58
|
+
` ${dim("A security boundary between your AI agents and everything they touch.")}`,
|
|
59
|
+
"",
|
|
60
|
+
` ${dim("This workspace is not protected yet. Three commands fix that:")}`,
|
|
61
|
+
"",
|
|
62
|
+
` ${cyan("1.")} ${bold("cirvix init")} ${dim("detect agents & MCP servers, write a policy, start protecting")}`,
|
|
63
|
+
` ${cyan("2.")} ${bold("cirvix demo")} ${dim("watch an injected exfiltration attempt get stopped, live")}`,
|
|
64
|
+
` ${cyan("3.")} ${bold("cirvix status")} ${dim("what is protected, what was blocked, at what cost")}`,
|
|
65
|
+
"",
|
|
66
|
+
` ${dim("Or see everything:")} ${bold("cirvix --help")}`,
|
|
67
|
+
"",
|
|
68
|
+
].join("\n"),
|
|
69
|
+
);
|
|
70
|
+
if (!creds) {
|
|
71
|
+
process.stdout.write(
|
|
72
|
+
[
|
|
73
|
+
"",
|
|
74
|
+
` ${dim("When you are ready to govern a whole team:")}`,
|
|
75
|
+
` ${bold("cirvix login")} ${dim(`link this machine · ${DASHBOARD_URL.replace("https://", "")}`)}`,
|
|
76
|
+
"",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Returning run: measured digest from the same source `cirvix status` uses. */
|
|
84
|
+
let digest = null;
|
|
85
|
+
try {
|
|
86
|
+
digest = (await statusCmd({ cwd })).output;
|
|
87
|
+
} catch {
|
|
88
|
+
digest = null;
|
|
89
|
+
}
|
|
90
|
+
if (digest) process.stdout.write(digest.trimEnd() + "\n\n");
|
|
91
|
+
|
|
92
|
+
const next = [];
|
|
93
|
+
if (digest && /RUNNING/i.test(String(digest))) {
|
|
94
|
+
next.push(["cirvix status", "the full picture: decisions, latency, approvals"]);
|
|
95
|
+
if (/approvals?\s+[1-9]/i.test(String(digest))) next.push(["cirvix approvals", "actions are waiting for a human decision"]);
|
|
96
|
+
next.push(["cirvix logs --last 10", "the most recent decisions, with reasons"]);
|
|
97
|
+
} else {
|
|
98
|
+
next.push(["cirvix init", "detect agents and start protecting this workspace"]);
|
|
99
|
+
}
|
|
100
|
+
if (!creds) next.push(["cirvix login", `link this machine to ${DEFAULT_CONTROL_PLANE.replace("https://", "")}`]);
|
|
101
|
+
|
|
102
|
+
const lines = next.map(([cmd, why]) => ` ${cyan("$")} ${bold(cmd.padEnd(22))} ${gray(why)}`);
|
|
103
|
+
process.stdout.write(panel({ title: "NEXT STEPS", lines }) + "\n\n");
|
|
104
|
+
return 0;
|
|
105
|
+
}
|