@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.
Files changed (81) hide show
  1. package/README.md +76 -17
  2. package/bin/cirvix.mjs +539 -85
  3. package/bin/escape-benchmark.mjs +67 -0
  4. package/package.json +36 -16
  5. package/src/adapters/base.mjs +150 -0
  6. package/src/adapters/claude-code.mjs +161 -0
  7. package/src/adapters/cline.mjs +107 -0
  8. package/src/adapters/codex.mjs +104 -0
  9. package/src/adapters/cursor.mjs +104 -0
  10. package/src/adapters/frameworks.mjs +110 -0
  11. package/src/adapters/gemini-cli.mjs +104 -0
  12. package/src/adapters/generic-mcp.mjs +101 -0
  13. package/src/adapters/index.mjs +209 -0
  14. package/src/adapters/roo-code.mjs +106 -0
  15. package/src/adapters/vscode.mjs +104 -0
  16. package/src/adapters/windsurf.mjs +107 -0
  17. package/src/commands/console.mjs +58 -0
  18. package/src/commands/demo.mjs +55 -124
  19. package/src/commands/doctor.mjs +235 -0
  20. package/src/commands/init.mjs +292 -30
  21. package/src/commands/interactive.mjs +690 -0
  22. package/src/commands/kill.mjs +74 -0
  23. package/src/commands/login.mjs +227 -0
  24. package/src/commands/onboard.mjs +52 -0
  25. package/src/commands/passport.mjs +149 -0
  26. package/src/commands/policy.mjs +10 -6
  27. package/src/commands/protect.mjs +293 -0
  28. package/src/commands/prove.mjs +209 -0
  29. package/src/commands/redteam.mjs +51 -0
  30. package/src/commands/scan.mjs +11 -9
  31. package/src/commands/shadow.mjs +62 -0
  32. package/src/commands/simulate.mjs +96 -0
  33. package/src/commands/status.mjs +122 -41
  34. package/src/commands/upgrade.mjs +11 -11
  35. package/src/commands/welcome.mjs +105 -0
  36. package/src/core/authority.mjs +909 -0
  37. package/src/core/baseline.mjs +97 -0
  38. package/src/core/config-store.mjs +280 -0
  39. package/src/core/cost.mjs +0 -0
  40. package/src/core/detect.mjs +4 -33
  41. package/src/core/entitlements.mjs +7 -24
  42. package/src/core/escape-benchmark.mjs +597 -0
  43. package/src/core/events.mjs +234 -0
  44. package/src/core/evidence.mjs +212 -0
  45. package/src/core/format.mjs +44 -18
  46. package/src/core/gateway.mjs +15 -211
  47. package/src/core/graph.mjs +270 -0
  48. package/src/core/guard.mjs +118 -4
  49. package/src/core/intent.mjs +166 -0
  50. package/src/core/journal.mjs +131 -40
  51. package/src/core/kill-switch.mjs +122 -0
  52. package/src/core/notices.mjs +22 -2
  53. package/src/core/packs.mjs +193 -0
  54. package/src/core/passport.mjs +555 -0
  55. package/src/core/pipeline.mjs +148 -6
  56. package/src/core/prompts.mjs +51 -0
  57. package/src/core/proof.mjs +440 -0
  58. package/src/core/redteam/index.mjs +185 -0
  59. package/src/core/referral.mjs +187 -0
  60. package/src/core/sandbox.mjs +139 -0
  61. package/src/core/session.mjs +172 -0
  62. package/src/core/shadow.mjs +95 -0
  63. package/src/core/theme.mjs +240 -0
  64. package/src/core/trifecta.mjs +321 -0
  65. package/src/core/ui/controller.mjs +192 -0
  66. package/src/core/ui/decisions.mjs +55 -0
  67. package/src/core/ui/index.mjs +49 -0
  68. package/src/core/ui/intercept.mjs +103 -0
  69. package/src/core/ui/live.mjs +51 -0
  70. package/src/core/ui/primitives.mjs +123 -0
  71. package/src/core/ui/theme.mjs +92 -0
  72. package/src/core/verified.mjs +108 -0
  73. package/src/core/windows.mjs +270 -0
  74. package/src/index.mjs +67 -0
  75. package/src/tui/activity.mjs +71 -0
  76. package/src/tui/app.mjs +292 -0
  77. package/src/tui/cards.mjs +235 -0
  78. package/src/tui/composer.mjs +88 -0
  79. package/src/tui/palette.mjs +48 -0
  80. package/src/tui/status.mjs +42 -0
  81. 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
+ }
@@ -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 { bold, dim, green, amber, blue, plural } from "../core/format.mjs";
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] overwrite an existing policy file
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({ cwd = process.cwd(), json = false, force = false } = {}) {
406
- const steps = [];
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
- const runtimes = await detectRuntimes();
423
- const servers = collectMcpServers(runtimes);
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) => ({ id: r.id, label: r.label, governed: r.governed, path: r.path })),
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 { result, output: render(result, { runtimes }) };
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 = ["", ` ${bold("CIRVIX")} ${dim("· initializing")}`, ""];
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
- lines.push(` ${green(bold("Cirvix is protecting your agent."))}`);
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
- // The one thing init deliberately does not do for you.
531
- const ungoverned = runtimes.filter((r) => !r.governed);
532
- if (ungoverned.length) {
533
- lines.push(` ${amber("One step left.")} ${dim(`${plural(ungoverned.length, "runtime")} still calls tools directly:`)}`);
534
- lines.push("");
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
- lines.push(` ${dim("Route them through the gateway — Cirvix does not edit your editor config for you:")}`);
782
+ } else {
783
+ lines.push(` ${green(bold("Ready. Your agent is under policy control."))}`);
540
784
  lines.push("");
541
- lines.push(` ${blue(`cirvix gateway --servers ${ungoverned[0].path}`)}`);
542
- lines.push("");
543
- lines.push(` ${dim("or add this to that file's mcpServers block:")}`);
544
- lines.push("");
545
- lines.push(dim(` "cirvix": { "command": "cirvix", "args": ["gateway", "--servers", "${ungoverned[0].path.replace(/\\/g, "/")}"] }`));
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("");