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.
@@ -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
- let result = (0, child_process_1.spawnSync)('claude', claudeArgs, {
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', claudeArgs, {
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.11"
2
+ "version": "1.14.12"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.11",
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"