@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,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
|
package/src/core/detect.mjs
CHANGED
|
@@ -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
|
|
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", "
|
|
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;
|