@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,97 @@
1
+ /**
2
+ * Agent Behavioral Baseline & Anomaly Detection.
3
+ *
4
+ * Tracks normal tools, endpoints, action frequencies, and resource patterns
5
+ * per agent, and detects significant statistical deviations.
6
+ *
7
+ * Example:
8
+ * FinanceBot normally uses: Stripe, Salesforce, PostgreSQL, Slack
9
+ * New behavior: AWS IAM, unknown MCP, external crypto API
10
+ * Verdict: BEHAVIORAL DEVIATION (Score: 97/100)
11
+ */
12
+
13
+ export class BehavioralBaseline {
14
+ constructor({
15
+ agentId,
16
+ normalTools = [],
17
+ normalDomains = [],
18
+ normalActions = [],
19
+ anomalyThreshold = 75,
20
+ minObservationsToEnforce = 10,
21
+ } = {}) {
22
+ this.agentId = agentId;
23
+ this.normalTools = new Set(normalTools);
24
+ this.normalDomains = new Set(normalDomains);
25
+ this.normalActions = new Set(normalActions);
26
+ this.anomalyThreshold = anomalyThreshold;
27
+ this.minObservationsToEnforce = minObservationsToEnforce;
28
+ this.observationCount = 0;
29
+ }
30
+
31
+ /**
32
+ * Learns from an observed action.
33
+ */
34
+ learn({ tool = null, domain = null, action = null }) {
35
+ this.observationCount += 1;
36
+ if (tool) this.normalTools.add(tool);
37
+ if (domain) this.normalDomains.add(domain);
38
+ if (action) this.normalActions.add(action);
39
+ }
40
+
41
+ /**
42
+ * Evaluates how severely an incoming action deviates from the baseline.
43
+ *
44
+ * @param {Object} call
45
+ * @param {string} call.tool
46
+ * @param {string} call.action
47
+ * @param {string} call.resource
48
+ * @returns {{ anomalyScore: number, isDeviation: boolean, reasons: string[] }}
49
+ */
50
+ scoreDeviation({ tool = null, action = null, resource = null }) {
51
+ if (this.observationCount < this.minObservationsToEnforce && this.normalTools.size === 0) {
52
+ return {
53
+ anomalyScore: 0,
54
+ isDeviation: false,
55
+ reasons: ["Insufficient baseline history to calculate deviation"],
56
+ };
57
+ }
58
+
59
+ let score = 0;
60
+ const reasons = [];
61
+
62
+ // Check unknown tool
63
+ if (tool && !this.normalTools.has(tool)) {
64
+ score += 45;
65
+ reasons.push(`Unknown tool '${tool}' never observed in baseline for agent ${this.agentId}`);
66
+ }
67
+
68
+ // Check unknown action
69
+ if (action && this.normalActions.size > 0 && !this.normalActions.has(action)) {
70
+ score += 25;
71
+ reasons.push(`Unusual action '${action}' outside baseline profile`);
72
+ }
73
+
74
+ // Check unknown external domain/destination in resource
75
+ if (resource && (resource.startsWith("http://") || resource.startsWith("https://"))) {
76
+ try {
77
+ const url = new URL(resource);
78
+ const host = url.hostname.toLowerCase();
79
+ if (this.normalDomains.size > 0 && !this.normalDomains.has(host)) {
80
+ score += 40;
81
+ reasons.push(`Unseen network destination '${host}' outside baseline domains`);
82
+ }
83
+ } catch {
84
+ // Not a standard URL
85
+ }
86
+ }
87
+
88
+ const anomalyScore = Math.min(100, score);
89
+ const isDeviation = anomalyScore >= this.anomalyThreshold;
90
+
91
+ return {
92
+ anomalyScore,
93
+ isDeviation,
94
+ reasons,
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Safe Configuration & Rollback Management for CIRVIX AgentControl.
3
+ *
4
+ * Ensures CIRVIX never silently damages, overwrites, or corrupts existing
5
+ * agent configurations (Claude Code, Cursor, Windsurf, Cline, Roo Code, etc.).
6
+ *
7
+ * Capabilities:
8
+ * - Non-destructive schema-aware JSON/JSONC parsing and serialization
9
+ * - Automated pre-modification backups (.cirvix/backups/<timestamp>/)
10
+ * - Safe transactional write with atomic replacement
11
+ * - Full rollback of previous configurations
12
+ * - Strict schema validation and circular-proxy detection
13
+ */
14
+
15
+ import { createHash } from "node:crypto";
16
+ import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
17
+ import { dirname, join, relative, resolve } from "node:path";
18
+ import { normalizeFsPath } from "./windows.mjs";
19
+
20
+ /**
21
+ * Strips comments from JSONC (JSON with comments) strings without external deps.
22
+ */
23
+ export function stripJsonComments(text) {
24
+ if (typeof text !== "string") return "";
25
+ let insideString = false;
26
+ let stringChar = "";
27
+ let isEscaped = false;
28
+ let result = "";
29
+
30
+ for (let i = 0; i < text.length; i++) {
31
+ const char = text[i];
32
+ const next = text[i + 1];
33
+
34
+ if (insideString) {
35
+ result += char;
36
+ if (isEscaped) {
37
+ isEscaped = false;
38
+ } else if (char === "\\") {
39
+ isEscaped = true;
40
+ } else if (char === stringChar) {
41
+ insideString = false;
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if (char === '"' || char === "'") {
47
+ insideString = true;
48
+ stringChar = char;
49
+ result += char;
50
+ continue;
51
+ }
52
+
53
+ // Line comment: //
54
+ if (char === "/" && next === "/") {
55
+ while (i < text.length && text[i] !== "\n") i++;
56
+ if (i < text.length) result += text[i]; // keep newline
57
+ continue;
58
+ }
59
+
60
+ // Block comment: /* ... */
61
+ if (char === "/" && next === "*") {
62
+ i += 2;
63
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
64
+ i++; // skip closing /
65
+ continue;
66
+ }
67
+
68
+ result += char;
69
+ }
70
+
71
+ // Remove trailing commas before } or ]
72
+ return result.replace(/,\s*([}\]])/g, "$1");
73
+ }
74
+
75
+ /**
76
+ * Parses JSON or JSONC safely. Returns null on parse error.
77
+ */
78
+ export function parseConfigJson(raw) {
79
+ if (typeof raw !== "string" || !raw.trim()) return null;
80
+ try {
81
+ return JSON.parse(raw);
82
+ } catch {
83
+ try {
84
+ const stripped = stripJsonComments(raw);
85
+ return JSON.parse(stripped);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Computes sha256 hash of a string or buffer.
94
+ */
95
+ export function sha256(content) {
96
+ return createHash("sha256").update(content).digest("hex");
97
+ }
98
+
99
+ /**
100
+ * Backup and Rollback Manager.
101
+ */
102
+ export class ConfigBackupManager {
103
+ constructor({ stateDir = join(process.cwd(), ".cirvix") } = {}) {
104
+ this.stateDir = stateDir;
105
+ this.backupDir = join(stateDir, "backups");
106
+ }
107
+
108
+ /**
109
+ * Creates a backup of one or more configuration files before modifying them.
110
+ *
111
+ * @param {string[]} filePaths
112
+ * @param {string} [reason="pre-integration"]
113
+ * @returns {Promise<{ backupId: string, timestamp: string, files: Array<{ path: string, backupPath: string, sha: string }> }>}
114
+ */
115
+ async createBackup(filePaths, reason = "pre-integration") {
116
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
117
+ const backupId = `backup-${timestamp}`;
118
+ const targetDir = join(this.backupDir, backupId);
119
+ await mkdir(targetDir, { recursive: true });
120
+
121
+ const backedUp = [];
122
+
123
+ for (const filePath of filePaths) {
124
+ const resolved = resolve(filePath);
125
+ try {
126
+ const content = await readFile(resolved);
127
+ const hash = sha256(content);
128
+ const safeName = normalizeFsPath(resolved).replace(/[:/\\]/g, "_");
129
+ const backupPath = join(targetDir, safeName);
130
+
131
+ await writeFile(backupPath, content);
132
+ backedUp.push({
133
+ path: resolved,
134
+ backupPath,
135
+ sha: hash,
136
+ });
137
+ } catch (err) {
138
+ // File may not exist yet; record as non-existent
139
+ backedUp.push({
140
+ path: resolved,
141
+ backupPath: null,
142
+ sha: null,
143
+ notExisted: true,
144
+ });
145
+ }
146
+ }
147
+
148
+ const manifest = {
149
+ backupId,
150
+ timestamp: new Date().toISOString(),
151
+ reason,
152
+ files: backedUp,
153
+ };
154
+
155
+ await writeFile(join(targetDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
156
+ return manifest;
157
+ }
158
+
159
+ /**
160
+ * Lists available backups, newest first.
161
+ */
162
+ async listBackups() {
163
+ try {
164
+ const entries = await readdir(this.backupDir, { withFileTypes: true });
165
+ const manifests = [];
166
+
167
+ for (const entry of entries) {
168
+ if (!entry.isDirectory() || !entry.name.startsWith("backup-")) continue;
169
+ const manifestPath = join(this.backupDir, entry.name, "manifest.json");
170
+ try {
171
+ const raw = await readFile(manifestPath, "utf8");
172
+ manifests.push(JSON.parse(raw));
173
+ } catch {}
174
+ }
175
+
176
+ return manifests.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
177
+ } catch {
178
+ return [];
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Restores configuration files from a backup.
184
+ *
185
+ * @param {string} [backupId] - Defaults to latest backup if omitted.
186
+ * @returns {Promise<{ restored: string[], removed: string[], backupId: string }>}
187
+ */
188
+ async rollback(backupId = null) {
189
+ const backups = await this.listBackups();
190
+ if (backups.length === 0) {
191
+ throw new Error("No CIRVIX backups found in " + this.backupDir);
192
+ }
193
+
194
+ const target = backupId ? backups.find((b) => b.backupId === backupId) : backups[0];
195
+ if (!target) {
196
+ throw new Error(`Backup "${backupId}" not found.`);
197
+ }
198
+
199
+ const restored = [];
200
+ const removed = [];
201
+
202
+ for (const file of target.files) {
203
+ if (file.notExisted) {
204
+ // The file did not exist before CIRVIX created it. Remove it cleanly.
205
+ try {
206
+ const { unlink } = await import("node:fs/promises");
207
+ await unlink(file.path);
208
+ removed.push(file.path);
209
+ } catch {}
210
+ } else if (file.backupPath) {
211
+ await mkdir(dirname(file.path), { recursive: true });
212
+ await copyFile(file.backupPath, file.path);
213
+ restored.push(file.path);
214
+ }
215
+ }
216
+
217
+ return { restored, removed, backupId: target.backupId };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Safe configuration patcher helper.
223
+ */
224
+ export class SafeConfigPatcher {
225
+ constructor(options = {}) {
226
+ this.backupManager = new ConfigBackupManager(options);
227
+ }
228
+
229
+ async patchJson(filePath, updateFn) {
230
+ const raw = await readFile(filePath, "utf8");
231
+ const parsed = parseConfigJson(raw) || {};
232
+ const updated = await updateFn(parsed);
233
+ await this.backupManager.createBackup([filePath]);
234
+ await writeFile(filePath, JSON.stringify(updated, null, 2), "utf8");
235
+ return updated;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Validates an MCP server map to ensure no circular references to cirvix
241
+ * and that each server has required fields.
242
+ */
243
+ export function validateMcpServersMap(serverMap) {
244
+ const errors = [];
245
+ if (!serverMap || typeof serverMap !== "object") {
246
+ errors.push("Server map must be an object.");
247
+ return { ok: false, errors };
248
+ }
249
+
250
+ for (const [name, def] of Object.entries(serverMap)) {
251
+ if (!def || typeof def !== "object") {
252
+ errors.push(`Server "${name}" definition must be an object.`);
253
+ continue;
254
+ }
255
+
256
+ if (name === "cirvix") {
257
+ // CIRVIX server itself must specify command and arguments
258
+ if (!def.command && !def.url) {
259
+ errors.push(`CIRVIX entry in server map must have a command or url.`);
260
+ }
261
+ continue;
262
+ }
263
+
264
+ if (!def.command && !def.url) {
265
+ errors.push(`Server "${name}" must define either "command" or "url".`);
266
+ }
267
+
268
+ // Circular check: does an upstream invoke cirvix gateway with circular arguments?
269
+ if (typeof def.command === "string" && def.command.includes("cirvix") && Array.isArray(def.args)) {
270
+ if (def.args.includes("gateway")) {
271
+ errors.push(`Server "${name}" circularly invokes "cirvix gateway".`);
272
+ }
273
+ }
274
+ }
275
+
276
+ return {
277
+ ok: errors.length === 0,
278
+ errors,
279
+ };
280
+ }
Binary file
@@ -15,6 +15,7 @@
15
15
  import { readFile, readdir, stat } from "node:fs/promises";
16
16
  import { homedir } from "node:os";
17
17
  import { join, resolve, sep } from "node:path";
18
+ import { detectFleet } from "../adapters/index.mjs";
18
19
 
19
20
  /** Best-effort read; a missing or unreadable file is simply "not present". */
20
21
  async function readJson(path) {
@@ -93,39 +94,9 @@ const FRAMEWORK_MARKERS = [
93
94
  { id: "mcp-sdk", label: "MCP SDK", deps: ["@modelcontextprotocol/sdk"] },
94
95
  ];
95
96
 
96
- export async function detectRuntimes() {
97
- const found = [];
98
-
99
- for (const probe of RUNTIME_PROBES) {
100
- // Merge across every config path a runtime uses rather than stopping at
101
- // the first that exists. Claude Code, for example, has both
102
- // ~/.claude/settings.json and ~/.claude.json, and MCP servers may live in
103
- // either — breaking early reports "0 MCP servers" for a machine that has
104
- // several, which is exactly the false clean bill this tool must not give.
105
- const paths = [];
106
- const servers = {};
107
-
108
- for (const path of probe.paths) {
109
- if (!(await exists(path))) continue;
110
- paths.push(path);
111
- const config = await readJson(path);
112
- Object.assign(servers, config?.[probe.mcpKey] ?? {});
113
- }
114
-
115
- if (paths.length === 0) continue;
116
-
117
- found.push({
118
- id: probe.id,
119
- label: probe.label,
120
- path: paths[0],
121
- paths,
122
- governed: isGoverned(servers),
123
- serverCount: Object.keys(servers).length,
124
- servers,
125
- });
126
- }
127
-
128
- return found;
97
+ export async function detectRuntimes(cwd = process.cwd(), options = {}) {
98
+ const { runtimes } = await detectFleet(cwd, options);
99
+ return runtimes;
129
100
  }
130
101
 
131
102
  /** A runtime is governed when its MCP traffic routes through the gateway. */
@@ -39,7 +39,7 @@
39
39
  */
40
40
 
41
41
  /** Ordered least → most capable. Used for `atLeast` comparisons. */
42
- export const TIER_ORDER = ["free", "lite", "starter", "pro", "team", "enterprise"];
42
+ export const TIER_ORDER = ["free", "starter", "pro", "team", "enterprise"];
43
43
 
44
44
  /**
45
45
  * `decisionsPerDay` is per SEAT for tiers where `perSeat` is true, and
@@ -96,29 +96,7 @@ export const TIERS = {
96
96
  shareableReplay: false,
97
97
  policyPacks: 2,
98
98
  sharedPolicy: false,
99
- },
100
- lite: {
101
- id: "lite",
102
- name: "Lite",
103
- // The $29 entry tier: five times Free's daily volume with a longer local
104
- // window, but no paid capabilities. Every figure sits between Free and
105
- // Starter on purpose — a cheaper tier that outranks a dearer one on any
106
- // axis silently un-sells that tier, so Lite is volume + retention only:
107
- // no persistent secrets, no replay, no approvals, no attestation, no
108
- // shared policy. The upgrade story stays one sentence: Starter adds
109
- // persistent secrets, replay and 3x the daily volume.
110
- decisionsPerDay: 500,
111
- perSeat: false,
112
- agents: 2,
113
- seatsIncluded: 1,
114
- auditRetentionHours: 24 * 3,
115
- persistentSecrets: false,
116
- secretTtlHours: 2,
117
- approvals: false,
118
- attestation: false,
119
- shareableReplay: false,
120
- policyPacks: 2,
121
- sharedPolicy: false,
99
+ customPolicies: false,
122
100
  },
123
101
  starter: {
124
102
  id: "starter",
@@ -135,6 +113,7 @@ export const TIERS = {
135
113
  shareableReplay: "local",
136
114
  policyPacks: 6,
137
115
  sharedPolicy: false,
116
+ customPolicies: true,
138
117
  },
139
118
  pro: {
140
119
  id: "pro",
@@ -151,6 +130,7 @@ export const TIERS = {
151
130
  shareableReplay: "full",
152
131
  policyPacks: null,
153
132
  sharedPolicy: "basic",
133
+ customPolicies: true,
154
134
  },
155
135
  team: {
156
136
  id: "team",
@@ -167,6 +147,7 @@ export const TIERS = {
167
147
  shareableReplay: "team",
168
148
  policyPacks: null,
169
149
  sharedPolicy: "full",
150
+ customPolicies: true,
170
151
  },
171
152
  enterprise: {
172
153
  id: "enterprise",
@@ -186,6 +167,7 @@ export const TIERS = {
186
167
  shareableReplay: "team",
187
168
  policyPacks: null,
188
169
  sharedPolicy: "full",
170
+ customPolicies: true,
189
171
  },
190
172
  };
191
173
 
@@ -292,6 +274,7 @@ export function nextTier(id) {
292
274
  export function can(licence, feature) {
293
275
  const tier = tierFor(licence?.tier);
294
276
  switch (feature) {
277
+ case "customPolicies": return tier.customPolicies === true;
295
278
  case "persistentSecrets": return tier.persistentSecrets === true;
296
279
  case "approvals": return tier.approvals === true;
297
280
  case "attestation": return tier.attestation === true;