fullcourtdefense-cli 1.14.11 → 1.14.12
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/dist/commands/discover.js +12 -0
- package/dist/commands/mcpGateway.d.ts +16 -0
- package/dist/commands/mcpGateway.js +141 -2
- package/dist/version.json +1 -1
- package/package.json +2 -1
|
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
|
|
|
43
43
|
const config_1 = require("../config");
|
|
44
44
|
const discoverProxy_1 = require("./discoverProxy");
|
|
45
45
|
const discoverSchedule_1 = require("./discoverSchedule");
|
|
46
|
+
const mcpGateway_1 = require("./mcpGateway");
|
|
46
47
|
const discoverPaths_1 = require("./discoverPaths");
|
|
47
48
|
const knownMcpServers_1 = require("./knownMcpServers");
|
|
48
49
|
const discoverAgentFiles_1 = require("./discoverAgentFiles");
|
|
@@ -770,6 +771,17 @@ async function discoverCommand(args, config) {
|
|
|
770
771
|
let clientCoverage = [];
|
|
771
772
|
if (runMcp) {
|
|
772
773
|
const candidates = candidateConfigPaths(cwd, args.extraPath);
|
|
774
|
+
// Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
|
|
775
|
+
// generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
|
|
776
|
+
// every discovery run converges the fleet to the per-server identity contract.
|
|
777
|
+
const healedIdentities = (0, mcpGateway_1.healGatewayAgentIdentities)(candidates.map(c => c.path).filter(p => fs.existsSync(p)));
|
|
778
|
+
if (healedIdentities.length > 0 && !silent) {
|
|
779
|
+
console.log(`${COLOR.yellow}Healed ${healedIdentities.length} stale gateway identit${healedIdentities.length === 1 ? 'y' : 'ies'} (old CLI defaults):${COLOR.reset}`);
|
|
780
|
+
for (const heal of healedIdentities) {
|
|
781
|
+
console.log(` ${COLOR.dim}•${COLOR.reset} ${heal.server}: ${heal.from} → ${heal.to} (${heal.file})`);
|
|
782
|
+
}
|
|
783
|
+
console.log(`${COLOR.gray}Restart the affected AI clients to pick up the corrected identity.${COLOR.reset}\n`);
|
|
784
|
+
}
|
|
773
785
|
for (const c of candidates) {
|
|
774
786
|
scanned.push({ path: c.path, source: c.source });
|
|
775
787
|
if (fs.existsSync(c.path)) {
|
|
@@ -63,6 +63,22 @@ export interface ProtectAllArgs extends McpGatewayArgs {
|
|
|
63
63
|
config?: string;
|
|
64
64
|
clients?: string;
|
|
65
65
|
}
|
|
66
|
+
export interface GatewayIdentityHealResult {
|
|
67
|
+
file: string;
|
|
68
|
+
server: string;
|
|
69
|
+
from: string;
|
|
70
|
+
to: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* SELF-HEAL for the policy-identity contract. Scans MCP client config files (JSON and Codex
|
|
74
|
+
* TOML) for FCD-gateway-wrapped servers whose baked-in `--agent-name` is a stale generic
|
|
75
|
+
* `<developer>-<client>` default, and rewrites it to `<developer>-<server>` so dashboard
|
|
76
|
+
* policies targeting the MCP server (`agentName contains <server>`) match at runtime.
|
|
77
|
+
*
|
|
78
|
+
* Runs automatically from `discover` and `protect-all`, so a customer machine with configs
|
|
79
|
+
* written by an old CLI version converges to the correct identity without manual edits.
|
|
80
|
+
*/
|
|
81
|
+
export declare function healGatewayAgentIdentities(files: string[], dryRun?: boolean): GatewayIdentityHealResult[];
|
|
66
82
|
export declare function protectAllCommand(args: ProtectAllArgs, config: BotGuardConfig): Promise<void>;
|
|
67
83
|
export declare function unprotectAllCommand(args: ProtectAllArgs, config: BotGuardConfig): Promise<void>;
|
|
68
84
|
export declare function installClaudeCodeMcpGatewayCommand(args: InstallClaudeCodeMcpGatewayArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -37,6 +37,7 @@ exports.ALL_GATEWAY_CLIENTS = void 0;
|
|
|
37
37
|
exports.mcpGatewayCommand = mcpGatewayCommand;
|
|
38
38
|
exports.installCursorMcpGatewayCommand = installCursorMcpGatewayCommand;
|
|
39
39
|
exports.installMcpGatewayCommand = installMcpGatewayCommand;
|
|
40
|
+
exports.healGatewayAgentIdentities = healGatewayAgentIdentities;
|
|
40
41
|
exports.protectAllCommand = protectAllCommand;
|
|
41
42
|
exports.unprotectAllCommand = unprotectAllCommand;
|
|
42
43
|
exports.installClaudeCodeMcpGatewayCommand = installClaudeCodeMcpGatewayCommand;
|
|
@@ -1142,6 +1143,129 @@ function backupFile(file) {
|
|
|
1142
1143
|
function perServerAgentName(developerName, _agentClient, serverName) {
|
|
1143
1144
|
return `${safeIdentityPart(developerName)}-${safeIdentityPart(serverName)}`;
|
|
1144
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Generic `<developer>-<client>` identities that pre-1.14.11 per-server installs baked into
|
|
1148
|
+
* configs. These are the ONLY values self-heal is allowed to rewrite — a custom --agent-name
|
|
1149
|
+
* an admin chose on purpose is never touched.
|
|
1150
|
+
*/
|
|
1151
|
+
const GENERIC_CLIENT_AGENT_SUFFIXES = ['cursor', 'claude-code', 'claude-desktop', 'codex', 'gemini', 'gemini-cli', 'windsurf', 'vscode'];
|
|
1152
|
+
/**
|
|
1153
|
+
* Rewrite a wrapped entry's `--agent-name` to the per-server contract (`<developer>-<server>`)
|
|
1154
|
+
* when it still carries a known-buggy generic client default. Mutates `args` in place.
|
|
1155
|
+
*/
|
|
1156
|
+
function healAgentNameInArgs(args, serverName) {
|
|
1157
|
+
const nameIdx = args.indexOf('--agent-name');
|
|
1158
|
+
if (nameIdx === -1 || nameIdx + 1 >= args.length)
|
|
1159
|
+
return { changed: false };
|
|
1160
|
+
const current = String(args[nameIdx + 1]);
|
|
1161
|
+
const devIdx = args.indexOf('--developer-name');
|
|
1162
|
+
const developerName = devIdx !== -1 && args[devIdx + 1] ? String(args[devIdx + 1]) : machineScopedUser();
|
|
1163
|
+
const devPart = safeIdentityPart(developerName);
|
|
1164
|
+
const expected = `${devPart}-${safeIdentityPart(serverName)}`;
|
|
1165
|
+
if (current === expected)
|
|
1166
|
+
return { changed: false };
|
|
1167
|
+
const isGenericDefault = GENERIC_CLIENT_AGENT_SUFFIXES.some(suffix => current === `${devPart}-${suffix}`);
|
|
1168
|
+
if (!isGenericDefault)
|
|
1169
|
+
return { changed: false };
|
|
1170
|
+
args[nameIdx + 1] = expected;
|
|
1171
|
+
return { changed: true, from: current, to: expected };
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* SELF-HEAL for the policy-identity contract. Scans MCP client config files (JSON and Codex
|
|
1175
|
+
* TOML) for FCD-gateway-wrapped servers whose baked-in `--agent-name` is a stale generic
|
|
1176
|
+
* `<developer>-<client>` default, and rewrites it to `<developer>-<server>` so dashboard
|
|
1177
|
+
* policies targeting the MCP server (`agentName contains <server>`) match at runtime.
|
|
1178
|
+
*
|
|
1179
|
+
* Runs automatically from `discover` and `protect-all`, so a customer machine with configs
|
|
1180
|
+
* written by an old CLI version converges to the correct identity without manual edits.
|
|
1181
|
+
*/
|
|
1182
|
+
function healGatewayAgentIdentities(files, dryRun = false) {
|
|
1183
|
+
const healed = [];
|
|
1184
|
+
const seen = new Set();
|
|
1185
|
+
for (const file of files) {
|
|
1186
|
+
let resolvedKey;
|
|
1187
|
+
try {
|
|
1188
|
+
resolvedKey = path.resolve(file).toLowerCase();
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (seen.has(resolvedKey))
|
|
1194
|
+
continue;
|
|
1195
|
+
seen.add(resolvedKey);
|
|
1196
|
+
if (/\.toml$/i.test(file)) {
|
|
1197
|
+
healed.push(...healCodexTomlAgentIdentities(file, dryRun));
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
let raw;
|
|
1201
|
+
try {
|
|
1202
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
let json;
|
|
1208
|
+
try {
|
|
1209
|
+
json = JSON.parse(raw);
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const maps = collectJsonServerMaps(json);
|
|
1215
|
+
let changed = false;
|
|
1216
|
+
for (const map of maps) {
|
|
1217
|
+
for (const [name, entry] of Object.entries(map)) {
|
|
1218
|
+
if (name === MANAGED_SERVER_NAME || !isGatewayWrappedEntry(entry))
|
|
1219
|
+
continue;
|
|
1220
|
+
const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
1221
|
+
const result = healAgentNameInArgs(args, name);
|
|
1222
|
+
if (result.changed) {
|
|
1223
|
+
entry.args = args;
|
|
1224
|
+
healed.push({ file, server: name, from: result.from, to: result.to });
|
|
1225
|
+
changed = true;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (changed && !dryRun) {
|
|
1230
|
+
backupFile(file);
|
|
1231
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`, 'utf8');
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return healed;
|
|
1235
|
+
}
|
|
1236
|
+
/** Codex config.toml variant of the identity self-heal (args are written as a JSON-style array). */
|
|
1237
|
+
function healCodexTomlAgentIdentities(file, dryRun) {
|
|
1238
|
+
let raw;
|
|
1239
|
+
try {
|
|
1240
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
const healed = [];
|
|
1246
|
+
const updated = raw.replace(/\[mcp_servers\.("?)([^\]"]+)\1\]([\s\S]*?)(?=\n\[|$)/g, (section, _q, serverName, body) => {
|
|
1247
|
+
if (serverName === MANAGED_SERVER_NAME || !body.includes('mcp-gateway'))
|
|
1248
|
+
return section;
|
|
1249
|
+
const newBody = body.replace(/("--agent-name",\s*")([^"]+)(")/, (m, pre, current, post) => {
|
|
1250
|
+
const devMatch = body.match(/"--developer-name",\s*"([^"]+)"/);
|
|
1251
|
+
const devPart = safeIdentityPart(devMatch?.[1] || machineScopedUser());
|
|
1252
|
+
const expected = `${devPart}-${safeIdentityPart(serverName)}`;
|
|
1253
|
+
if (current === expected)
|
|
1254
|
+
return m;
|
|
1255
|
+
const isGenericDefault = GENERIC_CLIENT_AGENT_SUFFIXES.some(suffix => current === `${devPart}-${suffix}`);
|
|
1256
|
+
if (!isGenericDefault)
|
|
1257
|
+
return m;
|
|
1258
|
+
healed.push({ file, server: serverName, from: current, to: expected });
|
|
1259
|
+
return `${pre}${expected}${post}`;
|
|
1260
|
+
});
|
|
1261
|
+
return section.replace(body, newBody);
|
|
1262
|
+
});
|
|
1263
|
+
if (healed.length > 0 && !dryRun) {
|
|
1264
|
+
backupFile(file);
|
|
1265
|
+
fs.writeFileSync(file, updated, 'utf8');
|
|
1266
|
+
}
|
|
1267
|
+
return healed;
|
|
1268
|
+
}
|
|
1145
1269
|
/**
|
|
1146
1270
|
* Per-server installs (`install-*-mcp-gateway --server-name foo`) must report the SAME
|
|
1147
1271
|
* runtime-agnostic agent identity that protect-all/auto-protect produce (`<developer>-<server>`).
|
|
@@ -1430,6 +1554,15 @@ async function protectAllCommand(args, config) {
|
|
|
1430
1554
|
return;
|
|
1431
1555
|
}
|
|
1432
1556
|
console.log(`\x1b[2mAuto-searching every known MCP client on this machine — ${files.length} config file(s) found.\x1b[0m\n`);
|
|
1557
|
+
// Converge stale generic gateway identities (old CLI defaults) to the per-server
|
|
1558
|
+
// contract before wrapping, so policy matching works on already-wrapped entries too.
|
|
1559
|
+
const healedIdentities = healGatewayAgentIdentities(files.map(f => f.path), dryRun);
|
|
1560
|
+
if (healedIdentities.length > 0) {
|
|
1561
|
+
console.log(`\x1b[32m✓ ${dryRun ? 'would fix' : 'fixed'} ${healedIdentities.length} stale gateway identit${healedIdentities.length === 1 ? 'y' : 'ies'}:\x1b[0m`);
|
|
1562
|
+
for (const heal of healedIdentities)
|
|
1563
|
+
console.log(` \x1b[2m•\x1b[0m ${heal.server}: ${heal.from} → ${heal.to}`);
|
|
1564
|
+
console.log('');
|
|
1565
|
+
}
|
|
1433
1566
|
let totalWrapped = 0;
|
|
1434
1567
|
let totalHealed = 0;
|
|
1435
1568
|
let totalManaged = 0;
|
|
@@ -1533,7 +1666,13 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1533
1666
|
includeShieldKey,
|
|
1534
1667
|
});
|
|
1535
1668
|
const claudeArgs = ['mcp', 'add', '--scope', scope, '--transport', 'stdio', serverName, '--', nodeExe, ...commandArgs];
|
|
1536
|
-
|
|
1669
|
+
// On Windows `claude` is a .cmd shim, so spawnSync needs shell:true — but with shell:true
|
|
1670
|
+
// Node does NOT quote args. Unquoted args with spaces/JSON (e.g. --mcp-args '["C:\\..."]',
|
|
1671
|
+
// --user-objective 'Developer requested...') get word-split by cmd.exe and written mangled
|
|
1672
|
+
// into the config. Quote them ourselves before handing the line to the shell.
|
|
1673
|
+
const shellQuote = (value) => (/[\s"]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value);
|
|
1674
|
+
const spawnClaudeArgs = process.platform === 'win32' ? claudeArgs.map(shellQuote) : claudeArgs;
|
|
1675
|
+
let result = (0, child_process_1.spawnSync)('claude', spawnClaudeArgs, {
|
|
1537
1676
|
stdio: 'inherit',
|
|
1538
1677
|
shell: process.platform === 'win32',
|
|
1539
1678
|
});
|
|
@@ -1543,7 +1682,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
1543
1682
|
shell: process.platform === 'win32',
|
|
1544
1683
|
});
|
|
1545
1684
|
if (removeResult.status === 0) {
|
|
1546
|
-
result = (0, child_process_1.spawnSync)('claude',
|
|
1685
|
+
result = (0, child_process_1.spawnSync)('claude', spawnClaudeArgs, {
|
|
1547
1686
|
stdio: 'inherit',
|
|
1548
1687
|
shell: process.platform === 'win32',
|
|
1549
1688
|
});
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.12",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"test:agent-file-posture": "npm run build && node scripts/test-agent-file-posture.js",
|
|
25
25
|
"test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
|
|
26
26
|
"test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
|
|
27
|
+
"test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
|
|
27
28
|
"test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
|
|
28
29
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
29
30
|
"prepublishOnly": "npm run build"
|