@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,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cirvix doctor` — diagnose this installation.
|
|
3
|
+
*
|
|
4
|
+
* Every check reports three states: OK, WARN (works but worth fixing) and
|
|
5
|
+
* FAIL (something is broken for real). The exit code is 1 only when at least
|
|
6
|
+
* one check FAILs — a warning should inform, not fail a CI job.
|
|
7
|
+
*
|
|
8
|
+
* Checks run from cheapest/safest to slowest, and the network is touched at
|
|
9
|
+
* most once: control-plane reachability is only probed when a credential file
|
|
10
|
+
* exists, with a hard timeout, because `doctor` must never hang a shell.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { access, readFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { parseRules } from "../core/policy.mjs";
|
|
18
|
+
import { loadPolicyFile } from "./policy.mjs";
|
|
19
|
+
import { detectFleet } from "../adapters/index.mjs";
|
|
20
|
+
import { resolveExecutable } from "../core/windows.mjs";
|
|
21
|
+
import { AuditChain } from "../core/audit.mjs";
|
|
22
|
+
import { UdsClient, defaultEndpoint, tokenPath } from "../core/uds.mjs";
|
|
23
|
+
import { bold, dim, green, red, amber, gray } from "../core/format.mjs";
|
|
24
|
+
import { panel } from "../core/ui/primitives.mjs";
|
|
25
|
+
|
|
26
|
+
async function exists(path) {
|
|
27
|
+
try {
|
|
28
|
+
await access(path);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One probe with a verdict. `fix` is shown verbatim when the check fails. */
|
|
36
|
+
function check(name, status, detail, fix) {
|
|
37
|
+
return { name, status: status ?? "warn", detail: detail ?? "", fix };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** GET with a hard timeout. Resolves { ok, status } and never rejects. */
|
|
41
|
+
async function probeUrl(url, timeoutMs = 4000) {
|
|
42
|
+
const ctrl = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
46
|
+
return { ok: res.ok, status: res.status };
|
|
47
|
+
} catch {
|
|
48
|
+
return { ok: false, status: null };
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function doctor({ cwd = process.cwd(), json = false } = {}) {
|
|
55
|
+
const stateDir = join(cwd, ".cirvix");
|
|
56
|
+
const credFile = join(homedir(), ".cirvix", "credentials.json");
|
|
57
|
+
const results = [];
|
|
58
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
59
|
+
|
|
60
|
+
/* 1 — runtime itself */
|
|
61
|
+
results.push(
|
|
62
|
+
nodeMajor >= 20
|
|
63
|
+
? check("Node runtime", "ok", `v${process.versions.node}`)
|
|
64
|
+
: check("Node runtime", "warn", `v${process.versions.node} — 20+ recommended`, "Upgrade Node to 20 or newer."),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/* 2 — workspace policy */
|
|
68
|
+
let policyPath = null;
|
|
69
|
+
for (const candidate of ["cirvix.policy", "cirvix.policy.json", ".cirvix/policy.json"]) {
|
|
70
|
+
if (await exists(join(cwd, candidate))) { policyPath = join(cwd, candidate); break; }
|
|
71
|
+
}
|
|
72
|
+
if (policyPath) {
|
|
73
|
+
try {
|
|
74
|
+
const loaded = await loadPolicyFile(policyPath, { cwd });
|
|
75
|
+
const rules = loaded.rules ?? [];
|
|
76
|
+
const testMsg = loaded.tests?.length ? `, ${loaded.tests.length} test${loaded.tests.length === 1 ? "" : "s"}` : "";
|
|
77
|
+
results.push(check("Policy file", "ok", `${rules.length} rule${rules.length === 1 ? "" : "s"}${testMsg} (${policyPath})`));
|
|
78
|
+
} catch (err) {
|
|
79
|
+
results.push(check("Policy file", "fail", `${policyPath}: ${String(err?.message ?? err).slice(0, 80)}`, "Check policy syntax, or run `cirvix init --force` to regenerate a starter policy."));
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
results.push(check("Policy file", "warn", "no cirvix.policy in this workspace", "Run `cirvix init` to detect agents and write a starter policy."));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* 2b — agent fleet detection */
|
|
86
|
+
try {
|
|
87
|
+
const fleet = await detectFleet(cwd, { stateDir });
|
|
88
|
+
const detected = fleet.runtimes ?? [];
|
|
89
|
+
if (detected.length === 0) {
|
|
90
|
+
results.push(check("Agent fleet", "ok", "no agent configurations detected (local/generic mode)"));
|
|
91
|
+
} else {
|
|
92
|
+
const ungoverned = detected.filter((d) => !d.isIntegrated);
|
|
93
|
+
if (ungoverned.length === 0) {
|
|
94
|
+
results.push(check("Agent fleet", "ok", `${detected.length} agent${detected.length === 1 ? "" : "s"} detected, all integrated`));
|
|
95
|
+
} else {
|
|
96
|
+
results.push(
|
|
97
|
+
check(
|
|
98
|
+
"Agent fleet",
|
|
99
|
+
"warn",
|
|
100
|
+
`${ungoverned.length} of ${detected.length} agent${detected.length === 1 ? "" : "s"} ungoverned (${ungoverned.map((u) => u.label).join(", ")})`,
|
|
101
|
+
"Run `cirvix init --apply` to automatically configure gateway interception.",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
results.push(check("Agent fleet", "warn", `fleet discovery error: ${String(err?.message ?? err).slice(0, 60)}`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* 2c — platform engine */
|
|
111
|
+
if (process.platform === "win32") {
|
|
112
|
+
const shell = resolveExecutable("cmd", { cwd }) || resolveExecutable("powershell", { cwd });
|
|
113
|
+
results.push(
|
|
114
|
+
check(
|
|
115
|
+
"Platform engine",
|
|
116
|
+
shell ? "ok" : "warn",
|
|
117
|
+
shell ? `Windows native (named pipes, PATHEXT, process-tree kill)` : "cmd/powershell not found in PATH",
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
results.push(check("Platform engine", "ok", `POSIX native (UDS sockets, signal lifecycle)`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* 3 — local state */
|
|
125
|
+
if (await exists(stateDir)) {
|
|
126
|
+
results.push(check("State directory", "ok", stateDir));
|
|
127
|
+
const auditPath = join(stateDir, "audit.jsonl");
|
|
128
|
+
if (await exists(auditPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const chain = new AuditChain(auditPath);
|
|
131
|
+
const verdict = await chain.verify();
|
|
132
|
+
/* verify() returns { ok, records, brokenAt, reason }. This read
|
|
133
|
+
`verdict.invalid ?? verdict.broken` — neither of which it has ever
|
|
134
|
+
returned — so `broken` was always `undefined > 0`, i.e. false, and
|
|
135
|
+
doctor reported "integrity verified" on a TAMPERED chain.
|
|
136
|
+
A false green in the one check whose entire job is detecting
|
|
137
|
+
tampering. */
|
|
138
|
+
const broken = verdict?.ok !== true;
|
|
139
|
+
results.push(
|
|
140
|
+
broken
|
|
141
|
+
? check(
|
|
142
|
+
"Audit chain",
|
|
143
|
+
"fail",
|
|
144
|
+
verdict?.reason
|
|
145
|
+
? String(verdict.reason).slice(0, 90)
|
|
146
|
+
: `chain breaks at record ${verdict?.brokenAt ?? "?"}`,
|
|
147
|
+
"Do not delete the chain. Run `cirvix audit verify` for the exact record and investigate before removing anything.",
|
|
148
|
+
)
|
|
149
|
+
: check("Audit chain", "ok", `${verdict.records} record${verdict.records === 1 ? "" : "s"} verified to genesis`),
|
|
150
|
+
);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
results.push(check("Audit chain", "warn", String(err?.message ?? err).slice(0, 80)));
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
results.push(check("Audit chain", "ok", "no decisions recorded yet"));
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
results.push(check("State directory", "warn", "no .cirvix/ in this workspace", "Run `cirvix init` — it creates state, a starter policy and a gateway config."));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* 4 — runtime liveness */
|
|
162
|
+
const endpoint = defaultEndpoint(stateDir);
|
|
163
|
+
if (await exists(tokenPath(stateDir))) {
|
|
164
|
+
try {
|
|
165
|
+
const client = new UdsClient(endpoint);
|
|
166
|
+
await client.ping();
|
|
167
|
+
results.push(check("Runtime daemon", "ok", endpoint));
|
|
168
|
+
} catch (err) {
|
|
169
|
+
results.push(
|
|
170
|
+
check("Runtime daemon", "warn", `not answering on ${endpoint} — ${String(err?.message ?? err).slice(0, 60)}`, "Start it with `cirvix init` (it launches the runtime) or `cirvix daemon`."),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
results.push(check("Runtime daemon", "ok", "not configured (no session token yet)"));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* 5 — control-plane credentials */
|
|
178
|
+
let creds = null;
|
|
179
|
+
if (await exists(credFile)) {
|
|
180
|
+
try {
|
|
181
|
+
creds = JSON.parse(await readFile(credFile, "utf8"));
|
|
182
|
+
if (creds && creds.apiKey && String(creds.apiKey).startsWith("cvx_")) {
|
|
183
|
+
results.push(check("Credentials", "ok", `${credFile} (key ${String(creds.apiKey).slice(0, 8)}…)`));
|
|
184
|
+
} else {
|
|
185
|
+
results.push(check("Credentials", "warn", `${credFile} has no usable apiKey`, "Re-run `cirvix login`."));
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
results.push(check("Credentials", "fail", `${credFile} is not valid JSON — ${String(err?.message ?? err).slice(0, 60)}`, "Delete the file and run `cirvix login` again."));
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
results.push(check("Credentials", "ok", "not linked (local-only mode — nothing to fix)"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* 6 — control plane reachability: only probed when a URL is configured */
|
|
195
|
+
const url = creds && creds.controlPlaneUrl ? String(creds.controlPlaneUrl).replace(/\/+$/, "") : null;
|
|
196
|
+
if (url) {
|
|
197
|
+
const probe = await probeUrl(`${url}/health`);
|
|
198
|
+
results.push(
|
|
199
|
+
probe.ok
|
|
200
|
+
? check("Control plane", "ok", `${url}/health → ${probe.status}`)
|
|
201
|
+
: check("Control plane", probe.status === null ? "warn" : "fail", `${url}/health → ${probe.status ?? "unreachable"}`, "Check your connection, or the deployment's tunnel/origin."),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const failed = results.filter((r) => r.status === "fail");
|
|
206
|
+
const warned = results.filter((r) => r.status === "warn");
|
|
207
|
+
|
|
208
|
+
if (json) {
|
|
209
|
+
process.stdout.write(JSON.stringify({ ok: failed.length === 0, results }, null, 2) + "\n");
|
|
210
|
+
return failed.length === 0 ? 0 : 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lines = results.map((r) => {
|
|
214
|
+
const mark = r.status === "ok" ? green("✓") : r.status === "fail" ? red("✗") : amber("!");
|
|
215
|
+
return ` ${mark} ${bold(r.name).padEnd(18)} ${gray(r.detail)}`;
|
|
216
|
+
});
|
|
217
|
+
if (!process.stdout.isTTY) {
|
|
218
|
+
// Accessible fallback: marks alone carry no meaning to a screen reader.
|
|
219
|
+
for (const r of results) process.stdout.write(`${r.status.toUpperCase().padEnd(5)} ${r.name} — ${r.detail}${r.fix ? ` (fix: ${r.fix})` : ""}\n`);
|
|
220
|
+
} else {
|
|
221
|
+
process.stdout.write(panel({ title: "CIRVIX DOCTOR", lines }) + "\n");
|
|
222
|
+
for (const r of results.filter((x) => x.fix)) {
|
|
223
|
+
process.stdout.write(` ${amber("→")} ${bold(r.name)}: ${r.fix}\n`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const summary = [
|
|
227
|
+
failed.length ? `${failed.length} failed` : null,
|
|
228
|
+
warned.length ? `${warned.length} warning${warned.length === 1 ? "" : "s"}` : null,
|
|
229
|
+
`${results.length - failed.length - warned.length} ok`,
|
|
230
|
+
]
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join(", ");
|
|
233
|
+
process.stdout.write(`\n ${dim(summary)}\n\n`);
|
|
234
|
+
return failed.length === 0 ? 0 : 1;
|
|
235
|
+
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
28
|
-
import { join } from "node:path";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
31
|
collectMcpServers,
|
|
@@ -34,8 +34,13 @@ import {
|
|
|
34
34
|
detectRuntimes,
|
|
35
35
|
} from "../core/detect.mjs";
|
|
36
36
|
import { compile } from "../core/policy-dsl.mjs";
|
|
37
|
-
import { writeToken, defaultEndpoint } from "../core/uds.mjs";
|
|
38
|
-
import {
|
|
37
|
+
import { writeToken, defaultEndpoint, tokenPath } from "../core/uds.mjs";
|
|
38
|
+
import { UdsClient } from "../core/uds.mjs";
|
|
39
|
+
import { bold, dim, green, red, amber, blue, cyan, gray, plural } from "../core/format.mjs";
|
|
40
|
+
import { shouldAnimate } from "../core/ui/controller.mjs";
|
|
41
|
+
import { brandHeader, panel } from "../core/ui/primitives.mjs";
|
|
42
|
+
import { ConfigBackupManager, validateMcpServersMap } from "../core/config-store.mjs";
|
|
43
|
+
import { detectFleet, generateFleetPlan } from "../adapters/index.mjs";
|
|
39
44
|
|
|
40
45
|
const STARTER_POLICY = `# Cirvix policy
|
|
41
46
|
#
|
|
@@ -249,6 +254,62 @@ require_approval:
|
|
|
249
254
|
approvers = developer
|
|
250
255
|
reason = "Agent configuration. Changing it changes what future runs are allowed to do."
|
|
251
256
|
|
|
257
|
+
require_approval:
|
|
258
|
+
name = approve-cursorrules-change
|
|
259
|
+
tool = filesystem.write
|
|
260
|
+
path = ./.cursorrules
|
|
261
|
+
approvers = developer
|
|
262
|
+
reason = "Cursor instructions. Changing it changes what future runs believe they were told."
|
|
263
|
+
|
|
264
|
+
require_approval:
|
|
265
|
+
name = approve-cursor-dir-change
|
|
266
|
+
tool = filesystem.write
|
|
267
|
+
path = ./.cursor/**
|
|
268
|
+
approvers = developer
|
|
269
|
+
reason = "Cursor agent configuration."
|
|
270
|
+
|
|
271
|
+
require_approval:
|
|
272
|
+
name = approve-windsurfrules-change
|
|
273
|
+
tool = filesystem.write
|
|
274
|
+
path = ./.windsurfrules
|
|
275
|
+
approvers = developer
|
|
276
|
+
reason = "Windsurf instructions. Changing it changes what future runs believe they were told."
|
|
277
|
+
|
|
278
|
+
require_approval:
|
|
279
|
+
name = approve-codeium-dir-change
|
|
280
|
+
tool = filesystem.write
|
|
281
|
+
path = ./.codeium/**
|
|
282
|
+
approvers = developer
|
|
283
|
+
reason = "Windsurf agent configuration."
|
|
284
|
+
|
|
285
|
+
require_approval:
|
|
286
|
+
name = approve-clinerules-change
|
|
287
|
+
tool = filesystem.write
|
|
288
|
+
path = ./.clinerules
|
|
289
|
+
approvers = developer
|
|
290
|
+
reason = "Cline instructions. Changing it changes what future runs believe they were told."
|
|
291
|
+
|
|
292
|
+
require_approval:
|
|
293
|
+
name = approve-roomodes-change
|
|
294
|
+
tool = filesystem.write
|
|
295
|
+
path = ./.roomodes
|
|
296
|
+
approvers = developer
|
|
297
|
+
reason = "Roo Code agent configuration."
|
|
298
|
+
|
|
299
|
+
require_approval:
|
|
300
|
+
name = approve-codex-config-change
|
|
301
|
+
tool = filesystem.write
|
|
302
|
+
path = ./codex.json
|
|
303
|
+
approvers = developer
|
|
304
|
+
reason = "Codex agent configuration."
|
|
305
|
+
|
|
306
|
+
require_approval:
|
|
307
|
+
name = approve-gemini-config-change
|
|
308
|
+
tool = filesystem.write
|
|
309
|
+
path = ./gemini.json
|
|
310
|
+
approvers = developer
|
|
311
|
+
reason = "Gemini agent configuration."
|
|
312
|
+
|
|
252
313
|
require_approval:
|
|
253
314
|
name = approve-database-write
|
|
254
315
|
tool = database.write
|
|
@@ -384,6 +445,16 @@ test "writing outside the workspace is denied":
|
|
|
384
445
|
tool = filesystem.write
|
|
385
446
|
path = /etc/hosts
|
|
386
447
|
expect deny
|
|
448
|
+
|
|
449
|
+
test "cursor rules modification requires approval":
|
|
450
|
+
tool = filesystem.write
|
|
451
|
+
path = ./.cursorrules
|
|
452
|
+
expect require_approval
|
|
453
|
+
|
|
454
|
+
test "cline rules modification requires approval":
|
|
455
|
+
tool = filesystem.write
|
|
456
|
+
path = ./.clinerules
|
|
457
|
+
expect require_approval
|
|
387
458
|
`;
|
|
388
459
|
|
|
389
460
|
async function exists(path) {
|
|
@@ -395,18 +466,86 @@ async function exists(path) {
|
|
|
395
466
|
}
|
|
396
467
|
}
|
|
397
468
|
|
|
469
|
+
async function probeRuntime(stateDir) {
|
|
470
|
+
const endpoint = defaultEndpoint(stateDir);
|
|
471
|
+
try {
|
|
472
|
+
await access(tokenPath(stateDir));
|
|
473
|
+
} catch {
|
|
474
|
+
return { running: false, endpoint, reason: "no session token" };
|
|
475
|
+
}
|
|
476
|
+
let token;
|
|
477
|
+
try {
|
|
478
|
+
token = (await readFile(tokenPath(stateDir), "utf8")).trim();
|
|
479
|
+
} catch {
|
|
480
|
+
return { running: false, endpoint, reason: "unreadable session token" };
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const client = new UdsClient({ endpoint, token, timeoutMs: 1200 });
|
|
484
|
+
const status = await client.call("cirvix/status", {});
|
|
485
|
+
return { running: true, endpoint, live: status };
|
|
486
|
+
} catch {
|
|
487
|
+
return { running: false, endpoint, reason: "nothing listening" };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
398
491
|
/**
|
|
399
492
|
* @param {object} opts
|
|
400
493
|
* @param {string} opts.cwd
|
|
401
494
|
* @param {boolean} [opts.json]
|
|
402
|
-
* @param {boolean} [opts.force]
|
|
495
|
+
* @param {boolean} [opts.force] overwrite an existing policy file
|
|
496
|
+
* @param {boolean} [opts.apply] apply integration plans to detected agents
|
|
497
|
+
* @param {boolean} [opts.dryRun] preview integration plans without modifying files
|
|
498
|
+
* @param {boolean|string} [opts.rollback] rollback to previous configuration
|
|
499
|
+
* @param {number} [opts.pace] animation pace; 0 disables
|
|
403
500
|
* @returns {Promise<{result:object, output:string}>}
|
|
404
501
|
*/
|
|
405
|
-
export async function init({
|
|
406
|
-
|
|
502
|
+
export async function init({
|
|
503
|
+
cwd = process.cwd(),
|
|
504
|
+
json = false,
|
|
505
|
+
force = false,
|
|
506
|
+
apply = false,
|
|
507
|
+
dryRun = false,
|
|
508
|
+
rollback = false,
|
|
509
|
+
pace,
|
|
510
|
+
} = {}) {
|
|
407
511
|
const stateDir = join(cwd, ".cirvix");
|
|
408
512
|
const policyPath = join(cwd, "cirvix.policy");
|
|
409
513
|
|
|
514
|
+
/* ------------------------------------------------------------ 0. rollback */
|
|
515
|
+
if (rollback) {
|
|
516
|
+
const backupManager = new ConfigBackupManager({ stateDir });
|
|
517
|
+
try {
|
|
518
|
+
const backupId = typeof rollback === "string" ? rollback : null;
|
|
519
|
+
const res = await backupManager.rollback(backupId);
|
|
520
|
+
const safe = {
|
|
521
|
+
ok: true,
|
|
522
|
+
action: "rollback",
|
|
523
|
+
backupId: res.backupId,
|
|
524
|
+
restored: res.restored,
|
|
525
|
+
removed: res.removed,
|
|
526
|
+
};
|
|
527
|
+
const output = json
|
|
528
|
+
? JSON.stringify(safe, null, 2)
|
|
529
|
+
: [
|
|
530
|
+
"",
|
|
531
|
+
` ${green(bold("✓ Configuration rolled back successfully"))}`,
|
|
532
|
+
` ${dim("Backup ID:")} ${res.backupId}`,
|
|
533
|
+
` ${dim("Restored:")} ${res.restored.length ? res.restored.join(", ") : "none"}`,
|
|
534
|
+
` ${dim("Removed:")} ${res.removed.length ? res.removed.join(", ") : "none"}`,
|
|
535
|
+
"",
|
|
536
|
+
].join("\n");
|
|
537
|
+
return { result: safe, output };
|
|
538
|
+
} catch (err) {
|
|
539
|
+
const safe = { ok: false, action: "rollback", error: err.message };
|
|
540
|
+
const output = json
|
|
541
|
+
? JSON.stringify(safe, null, 2)
|
|
542
|
+
: `\n ${red(bold("✗ Rollback failed:"))} ${err.message}\n`;
|
|
543
|
+
return { result: safe, output };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const steps = [];
|
|
548
|
+
|
|
410
549
|
/* ------------------------------------------------------------ 1. runtime */
|
|
411
550
|
await mkdir(stateDir, { recursive: true });
|
|
412
551
|
const token = await writeToken(stateDir);
|
|
@@ -419,8 +558,9 @@ export async function init({ cwd = process.cwd(), json = false, force = false }
|
|
|
419
558
|
});
|
|
420
559
|
|
|
421
560
|
/* -------------------------------------------------------- 2. MCP servers */
|
|
422
|
-
|
|
423
|
-
|
|
561
|
+
let fleet = await detectFleet(cwd, { stateDir });
|
|
562
|
+
let runtimes = fleet.runtimes;
|
|
563
|
+
const servers = fleet.mcpServers;
|
|
424
564
|
steps.push({
|
|
425
565
|
id: "mcp",
|
|
426
566
|
label: "MCP servers detected",
|
|
@@ -431,7 +571,7 @@ export async function init({ cwd = process.cwd(), json = false, force = false }
|
|
|
431
571
|
});
|
|
432
572
|
|
|
433
573
|
/* ------------------------------------------------------------- 3. agents */
|
|
434
|
-
const frameworks = await detectFrameworks(cwd);
|
|
574
|
+
const frameworks = fleet.frameworks?.length ? fleet.frameworks : await detectFrameworks(cwd);
|
|
435
575
|
const agentNames = [...runtimes.map((r) => r.label), ...frameworks.map((f) => f.label)];
|
|
436
576
|
steps.push({
|
|
437
577
|
id: "agents",
|
|
@@ -487,8 +627,61 @@ export async function init({ cwd = process.cwd(), json = false, force = false }
|
|
|
487
627
|
detail: ".cirvix/audit.jsonl · hash-chained, verify with `cirvix audit verify`",
|
|
488
628
|
});
|
|
489
629
|
|
|
630
|
+
/* --------------------------------------------------- 7. fleet integration */
|
|
631
|
+
const fleetPlans = await generateFleetPlan(cwd, { stateDir });
|
|
632
|
+
const unintegratedPlans = fleetPlans.filter((p) => {
|
|
633
|
+
const rt = runtimes.find((r) => r.id === p.adapterId);
|
|
634
|
+
return rt && !rt.isIntegrated && p.canIntegrate;
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
let integrationBackup = null;
|
|
638
|
+
let appliedCount = 0;
|
|
639
|
+
|
|
640
|
+
if (apply && unintegratedPlans.length > 0) {
|
|
641
|
+
const backupManager = new ConfigBackupManager({ stateDir });
|
|
642
|
+
const targetFiles = unintegratedPlans.map((p) => p.targetFile);
|
|
643
|
+
integrationBackup = await backupManager.createBackup(targetFiles, "cirvix init --apply");
|
|
644
|
+
|
|
645
|
+
for (const plan of unintegratedPlans) {
|
|
646
|
+
const targetPath = plan.targetFile;
|
|
647
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
648
|
+
await writeFile(targetPath, JSON.stringify(plan.plan, null, 2), "utf8");
|
|
649
|
+
appliedCount++;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Refresh detection after integration
|
|
653
|
+
fleet = await detectFleet(cwd, { stateDir });
|
|
654
|
+
runtimes = fleet.runtimes;
|
|
655
|
+
|
|
656
|
+
steps.push({
|
|
657
|
+
id: "integration",
|
|
658
|
+
label: "Fleet integration applied",
|
|
659
|
+
ok: true,
|
|
660
|
+
detail: `${plural(appliedCount, "agent")} wired into CIRVIX · backup ${integrationBackup.backupId}`,
|
|
661
|
+
});
|
|
662
|
+
} else if (dryRun) {
|
|
663
|
+
steps.push({
|
|
664
|
+
id: "integration",
|
|
665
|
+
label: "Fleet integration (dry run)",
|
|
666
|
+
ok: true,
|
|
667
|
+
detail: `${plural(unintegratedPlans.length, "agent")} ready to wire (no files modified)`,
|
|
668
|
+
});
|
|
669
|
+
} else {
|
|
670
|
+
steps.push({
|
|
671
|
+
id: "integration",
|
|
672
|
+
label: "Fleet integration plan",
|
|
673
|
+
ok: true,
|
|
674
|
+
detail: unintegratedPlans.length
|
|
675
|
+
? `${plural(unintegratedPlans.length, "agent")} ready for automatic integration (use --apply)`
|
|
676
|
+
: "all detected agents already routed through CIRVIX",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Probe whether a runtime is actually reachable.
|
|
681
|
+
const runtimeProbe = await probeRuntime(stateDir);
|
|
682
|
+
|
|
490
683
|
const result = {
|
|
491
|
-
ok: steps.every((s) => s.ok || s.id === "mcp" || s.id === "agents"),
|
|
684
|
+
ok: steps.every((s) => s.ok || s.id === "mcp" || s.id === "agents" || s.id === "integration"),
|
|
492
685
|
cwd,
|
|
493
686
|
stateDir,
|
|
494
687
|
policyPath,
|
|
@@ -497,9 +690,24 @@ export async function init({ cwd = process.cwd(), json = false, force = false }
|
|
|
497
690
|
rules: ruleCount,
|
|
498
691
|
tests: testCount,
|
|
499
692
|
mcpServers: servers.length,
|
|
500
|
-
runtimes: runtimes.map((r) => ({
|
|
693
|
+
runtimes: runtimes.map((r) => ({
|
|
694
|
+
id: r.id,
|
|
695
|
+
label: r.label,
|
|
696
|
+
governed: r.governed,
|
|
697
|
+
compatibilityLevel: r.compatibilityLevel,
|
|
698
|
+
path: r.path,
|
|
699
|
+
})),
|
|
501
700
|
credentials: credentials.length,
|
|
502
701
|
steps,
|
|
702
|
+
runtime: runtimeProbe,
|
|
703
|
+
fleetPlans: unintegratedPlans.map((p) => ({
|
|
704
|
+
adapterId: p.adapterId,
|
|
705
|
+
label: p.label,
|
|
706
|
+
targetFile: p.targetFile,
|
|
707
|
+
snippet: p.snippet,
|
|
708
|
+
})),
|
|
709
|
+
appliedCount,
|
|
710
|
+
backupId: integrationBackup?.backupId ?? null,
|
|
503
711
|
// Never printed, never logged — returned so a caller that just created the
|
|
504
712
|
// session can use it without reading the file back.
|
|
505
713
|
token,
|
|
@@ -507,47 +715,101 @@ export async function init({ cwd = process.cwd(), json = false, force = false }
|
|
|
507
715
|
|
|
508
716
|
if (json) {
|
|
509
717
|
const { token: _hidden, ...safe } = result;
|
|
510
|
-
return { result, output: JSON.stringify(safe, null, 2) };
|
|
718
|
+
return { result: safe, output: JSON.stringify(safe, null, 2) };
|
|
511
719
|
}
|
|
512
|
-
return {
|
|
720
|
+
return {
|
|
721
|
+
result,
|
|
722
|
+
output: render(result, { runtimes, runtimeProbe, appliedCount, backupId: integrationBackup?.backupId, dryRun }),
|
|
723
|
+
};
|
|
513
724
|
}
|
|
514
725
|
|
|
515
726
|
/* -------------------------------------------------------------------------- */
|
|
516
727
|
|
|
517
|
-
function render(result, { runtimes }) {
|
|
518
|
-
const lines = [
|
|
728
|
+
function render(result, { runtimes, runtimeProbe, appliedCount = 0, backupId = null, dryRun = false }) {
|
|
729
|
+
const lines = [];
|
|
730
|
+
lines.push("");
|
|
731
|
+
lines.push(brandHeader({ width: 62 }));
|
|
732
|
+
lines.push("");
|
|
519
733
|
|
|
734
|
+
// Initialization steps — premium checkmarks, real data.
|
|
735
|
+
lines.push(` ${dim("Initializing CIRVIX runtime...")}`);
|
|
520
736
|
const width = Math.max(...result.steps.map((s) => s.label.length));
|
|
521
737
|
for (const step of result.steps) {
|
|
522
738
|
const tick = step.ok ? green("✓") : amber("○");
|
|
523
739
|
lines.push(` ${tick} ${step.label.padEnd(width)} ${dim(step.detail)}`);
|
|
524
740
|
}
|
|
525
|
-
|
|
526
741
|
lines.push("");
|
|
527
|
-
|
|
742
|
+
|
|
743
|
+
// Protected panel
|
|
744
|
+
const isOnline = Boolean(runtimeProbe?.running);
|
|
745
|
+
const runtimeLabel = isOnline ? green(bold("● ONLINE")) : cyan(bold("● CONFIGURED"));
|
|
746
|
+
const runtimeDetail = isOnline ? dim("control socket reachable") : dim("state configured — start runtime to go ONLINE");
|
|
747
|
+
const policyDetail = `${result.rules} rules loaded`;
|
|
748
|
+
const testsDetail = `${result.tests} policy tests`;
|
|
749
|
+
const secretsDetail = result.credentials ? `${result.credentials} credential sources` : "active";
|
|
750
|
+
|
|
751
|
+
const panelLines = [
|
|
752
|
+
`${bold("CIRVIX PROTECTED")}`,
|
|
753
|
+
``,
|
|
754
|
+
`${"Runtime".padEnd(12)} ${runtimeLabel} ${runtimeDetail}`,
|
|
755
|
+
`${"Policy".padEnd(12)} ${green(bold("● ENFORCING"))} ${dim(policyDetail)}`,
|
|
756
|
+
`${"Secrets".padEnd(12)} ${green(bold("● PROTECTED"))} ${dim(secretsDetail)}`,
|
|
757
|
+
`${"Audit".padEnd(12)} ${green(bold("● RECORDING"))} ${dim(".cirvix/audit.jsonl")}`,
|
|
758
|
+
`${"Agents".padEnd(12)} ${String(runtimes.length)} detected`,
|
|
759
|
+
];
|
|
760
|
+
|
|
761
|
+
if (runtimes.length > 0) {
|
|
762
|
+
panelLines.push(``);
|
|
763
|
+
for (const rt of runtimes) {
|
|
764
|
+
const badge = rt.governed ? green(bold("● " + (rt.compatibilityLevel ?? "INTEGRATED"))) : amber("○ " + (rt.compatibilityLevel ?? "CONFIGURABLE"));
|
|
765
|
+
panelLines.push(` ${rt.label.padEnd(16)} ${badge}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
panelLines.push(``);
|
|
770
|
+
panelLines.push(`${dim(`${0} blocked · ${0} approvals · ${0} violations`)}`);
|
|
771
|
+
|
|
772
|
+
lines.push(panel({ lines: panelLines, width: 62 }));
|
|
528
773
|
lines.push("");
|
|
529
774
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
for (const r of ungoverned) {
|
|
536
|
-
lines.push(` ${bold(r.label)} ${dim(r.path)}`);
|
|
775
|
+
if (appliedCount > 0) {
|
|
776
|
+
lines.push(` ${green(bold(`✓ Successfully wired ${appliedCount} agent(s) into CIRVIX gateway.`))}`);
|
|
777
|
+
if (backupId) {
|
|
778
|
+
lines.push(` ${dim(`Safe backup saved as: ${backupId}`)}`);
|
|
779
|
+
lines.push(` ${dim("To undo this change at any time, run:")} ${blue("cirvix init --rollback")}`);
|
|
537
780
|
}
|
|
538
781
|
lines.push("");
|
|
539
|
-
|
|
782
|
+
} else {
|
|
783
|
+
lines.push(` ${green(bold("Ready. Your agent is under policy control."))}`);
|
|
540
784
|
lines.push("");
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Integration guidance
|
|
788
|
+
const ungoverned = runtimes.filter((r) => !r.governed);
|
|
789
|
+
if (ungoverned.length && !appliedCount) {
|
|
790
|
+
const warnLines = [
|
|
791
|
+
`${amber(bold("⚠ INTEGRATION ACTION AVAILABLE"))}`,
|
|
792
|
+
``,
|
|
793
|
+
`${plural(ungoverned.length, "agent")} detected but not yet routed through`,
|
|
794
|
+
`the CIRVIX gateway:`,
|
|
795
|
+
``,
|
|
796
|
+
...ungoverned.map((u) => ` • ${u.label} (${u.compatibilityLevel ?? "CONFIGURABLE"})`),
|
|
797
|
+
``,
|
|
798
|
+
`Automatic non-destructive integration:`,
|
|
799
|
+
` ${blue("cirvix init --apply")} ${dim("(creates pre-modification backup)")}`,
|
|
800
|
+
` ${blue("cirvix init --dry-run")} ${dim("(preview configuration without writing)")}`,
|
|
801
|
+
` ${blue("cirvix init --rollback")} ${dim("(revert to pre-integration state)")}`,
|
|
802
|
+
``,
|
|
803
|
+
`Or manually run:`,
|
|
804
|
+
` ${blue(`cirvix gateway --servers ${ungoverned[0].path}`)}`,
|
|
805
|
+
];
|
|
806
|
+
lines.push(panel({ lines: warnLines, width: 62, heavy: true }));
|
|
546
807
|
lines.push("");
|
|
547
808
|
}
|
|
548
809
|
|
|
549
810
|
lines.push(` ${dim("Next")}`);
|
|
550
811
|
lines.push(` ${blue("cirvix policy test")} ${dim("run the policy's own test cases")}`);
|
|
812
|
+
lines.push(` ${blue("cirvix scan")} ${dim("full inventory of runtimes, MCP servers & secrets")}`);
|
|
551
813
|
lines.push(` ${blue("cirvix demo")} ${dim("watch an injected exfiltration attempt get stopped")}`);
|
|
552
814
|
lines.push(` ${blue("cirvix status")} ${dim("what is protected right now")}`);
|
|
553
815
|
lines.push("");
|