@cirvix_ai/agent-control 0.1.2 → 0.1.5

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 (70) hide show
  1. package/README.md +76 -17
  2. package/bin/cirvix.mjs +488 -40
  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/demo.mjs +56 -70
  18. package/src/commands/doctor.mjs +235 -0
  19. package/src/commands/init.mjs +292 -30
  20. package/src/commands/interactive.mjs +690 -0
  21. package/src/commands/kill.mjs +74 -0
  22. package/src/commands/login.mjs +227 -0
  23. package/src/commands/passport.mjs +149 -0
  24. package/src/commands/policy.mjs +10 -6
  25. package/src/commands/protect.mjs +293 -0
  26. package/src/commands/prove.mjs +209 -0
  27. package/src/commands/redteam.mjs +51 -0
  28. package/src/commands/scan.mjs +6 -4
  29. package/src/commands/shadow.mjs +62 -0
  30. package/src/commands/simulate.mjs +96 -0
  31. package/src/commands/status.mjs +121 -36
  32. package/src/commands/upgrade.mjs +17 -9
  33. package/src/commands/welcome.mjs +105 -0
  34. package/src/core/authority.mjs +909 -0
  35. package/src/core/baseline.mjs +97 -0
  36. package/src/core/config-store.mjs +280 -0
  37. package/src/core/cost.mjs +0 -0
  38. package/src/core/detect.mjs +4 -33
  39. package/src/core/entitlements.mjs +6 -0
  40. package/src/core/escape-benchmark.mjs +597 -0
  41. package/src/core/evidence.mjs +212 -0
  42. package/src/core/format.mjs +27 -0
  43. package/src/core/gateway.mjs +15 -211
  44. package/src/core/graph.mjs +270 -0
  45. package/src/core/guard.mjs +118 -4
  46. package/src/core/intent.mjs +166 -0
  47. package/src/core/journal.mjs +131 -40
  48. package/src/core/kill-switch.mjs +122 -0
  49. package/src/core/notices.mjs +22 -2
  50. package/src/core/packs.mjs +193 -0
  51. package/src/core/passport.mjs +555 -0
  52. package/src/core/pipeline.mjs +148 -6
  53. package/src/core/prompts.mjs +51 -0
  54. package/src/core/proof.mjs +440 -0
  55. package/src/core/redteam/index.mjs +185 -0
  56. package/src/core/referral.mjs +187 -0
  57. package/src/core/sandbox.mjs +139 -0
  58. package/src/core/session.mjs +172 -0
  59. package/src/core/shadow.mjs +95 -0
  60. package/src/core/trifecta.mjs +321 -0
  61. package/src/core/ui/controller.mjs +192 -0
  62. package/src/core/ui/decisions.mjs +55 -0
  63. package/src/core/ui/index.mjs +49 -0
  64. package/src/core/ui/intercept.mjs +103 -0
  65. package/src/core/ui/live.mjs +51 -0
  66. package/src/core/ui/primitives.mjs +123 -0
  67. package/src/core/ui/theme.mjs +92 -0
  68. package/src/core/verified.mjs +108 -0
  69. package/src/core/windows.mjs +270 -0
  70. package/src/index.mjs +25 -0
@@ -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. */
@@ -96,6 +96,7 @@ export const TIERS = {
96
96
  shareableReplay: false,
97
97
  policyPacks: 2,
98
98
  sharedPolicy: false,
99
+ customPolicies: false,
99
100
  },
100
101
  starter: {
101
102
  id: "starter",
@@ -112,6 +113,7 @@ export const TIERS = {
112
113
  shareableReplay: "local",
113
114
  policyPacks: 6,
114
115
  sharedPolicy: false,
116
+ customPolicies: true,
115
117
  },
116
118
  pro: {
117
119
  id: "pro",
@@ -128,6 +130,7 @@ export const TIERS = {
128
130
  shareableReplay: "full",
129
131
  policyPacks: null,
130
132
  sharedPolicy: "basic",
133
+ customPolicies: true,
131
134
  },
132
135
  team: {
133
136
  id: "team",
@@ -144,6 +147,7 @@ export const TIERS = {
144
147
  shareableReplay: "team",
145
148
  policyPacks: null,
146
149
  sharedPolicy: "full",
150
+ customPolicies: true,
147
151
  },
148
152
  enterprise: {
149
153
  id: "enterprise",
@@ -163,6 +167,7 @@ export const TIERS = {
163
167
  shareableReplay: "team",
164
168
  policyPacks: null,
165
169
  sharedPolicy: "full",
170
+ customPolicies: true,
166
171
  },
167
172
  };
168
173
 
@@ -269,6 +274,7 @@ export function nextTier(id) {
269
274
  export function can(licence, feature) {
270
275
  const tier = tierFor(licence?.tier);
271
276
  switch (feature) {
277
+ case "customPolicies": return tier.customPolicies === true;
272
278
  case "persistentSecrets": return tier.persistentSecrets === true;
273
279
  case "approvals": return tier.approvals === true;
274
280
  case "attestation": return tier.attestation === true;