@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.
- package/README.md +76 -17
- package/bin/cirvix.mjs +488 -40
- 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/demo.mjs +56 -70
- 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/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 +6 -4
- package/src/commands/shadow.mjs +62 -0
- package/src/commands/simulate.mjs +96 -0
- package/src/commands/status.mjs +121 -36
- package/src/commands/upgrade.mjs +17 -9
- 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 +6 -0
- package/src/core/escape-benchmark.mjs +597 -0
- package/src/core/evidence.mjs +212 -0
- package/src/core/format.mjs +27 -0
- 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/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 +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
|
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. */
|
|
@@ -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;
|