fullcourtdefense-cli 1.34.21 → 1.34.25

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 (91) hide show
  1. package/README.md +1 -0
  2. package/dist/actionIdentity.d.ts +1 -0
  3. package/dist/actionIdentity.js +2 -0
  4. package/dist/actionPolicyEngine.d.ts +5 -0
  5. package/dist/actionPolicyEngine.js +273 -30
  6. package/dist/agentTerminal.d.ts +1 -3
  7. package/dist/agentTerminal.js +14 -29
  8. package/dist/approvalPrompt.d.ts +7 -0
  9. package/dist/approvalPrompt.js +46 -0
  10. package/dist/askDialog.d.ts +87 -0
  11. package/dist/askDialog.js +209 -0
  12. package/dist/blockExplanation.js +18 -6
  13. package/dist/boundaryVerification.d.ts +20 -0
  14. package/dist/boundaryVerification.js +147 -0
  15. package/dist/commands/daemon.js +198 -70
  16. package/dist/commands/deterministicGuard.d.ts +2 -0
  17. package/dist/commands/deterministicGuard.js +161 -9
  18. package/dist/commands/hook.js +81 -29
  19. package/dist/commands/installClaudeHook.js +11 -5
  20. package/dist/commands/installCursorHook.js +5 -4
  21. package/dist/commands/mcpGateway.d.ts +2 -0
  22. package/dist/commands/mcpGateway.js +163 -56
  23. package/dist/commands/onboard.js +6 -5
  24. package/dist/commands/onboardingSummary.d.ts +5 -0
  25. package/dist/commands/onboardingSummary.js +11 -0
  26. package/dist/commands/protectedRun.d.ts +14 -0
  27. package/dist/commands/protectedRun.js +207 -0
  28. package/dist/containment-runtime/index.mjs +30118 -0
  29. package/dist/containment-runtime/licenses/_anthropic-ai_sandbox-runtime.txt +201 -0
  30. package/dist/containment-runtime/licenses/_pondwader_socks5-server.txt +20 -0
  31. package/dist/containment-runtime/licenses/commander.txt +22 -0
  32. package/dist/containment-runtime/licenses/node-forge.txt +331 -0
  33. package/dist/containment-runtime/licenses/zod.txt +21 -0
  34. package/dist/containment-runtime/runtime-version.json +1 -0
  35. package/dist/containment-runtime/vendor/java-proxy-agent/build.ts +95 -0
  36. package/dist/containment-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
  37. package/dist/containment-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
  38. package/dist/containment-runtime/vendor/seccomp/build.ts +69 -0
  39. package/dist/containment-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
  40. package/dist/containment-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
  41. package/dist/containment-runtime/vendor/srt-win/build.ts +21 -0
  42. package/dist/containment-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
  43. package/dist/containmentLease.d.ts +5 -0
  44. package/dist/containmentLease.js +72 -0
  45. package/dist/detectionUpdates.d.ts +23 -3
  46. package/dist/detectionUpdates.js +45 -14
  47. package/dist/detectorArtifact.d.ts +17 -0
  48. package/dist/detectorArtifact.js +35 -0
  49. package/dist/detectorBaseline.d.ts +2 -0
  50. package/dist/detectorBaseline.js +5 -0
  51. package/dist/detectorModule.d.ts +9 -0
  52. package/dist/detectorModule.js +84 -0
  53. package/dist/detectorRuntime.d.ts +21 -0
  54. package/dist/detectorRuntime.js +49 -0
  55. package/dist/detectorUpdates.d.ts +21 -0
  56. package/dist/detectorUpdates.js +86 -0
  57. package/dist/devConfirm.d.ts +97 -15
  58. package/dist/devConfirm.js +447 -117
  59. package/dist/distress.d.ts +2 -0
  60. package/dist/distress.js +2 -0
  61. package/dist/frictionRollout.js +103 -0
  62. package/dist/gatewayRuntime.d.ts +19 -0
  63. package/dist/gatewayRuntime.js +93 -0
  64. package/dist/hookSlim.js +17 -18
  65. package/dist/index.js +35 -0
  66. package/dist/localDetectionUpdates.d.ts +3 -1
  67. package/dist/localDetectionUpdates.js +8 -1
  68. package/dist/localSafetySnapshot.d.ts +9 -5
  69. package/dist/localSafetySnapshot.js +42 -15
  70. package/dist/mcpToolReview.d.ts +17 -0
  71. package/dist/mcpToolReview.js +127 -0
  72. package/dist/mcpToolTrust.d.ts +22 -0
  73. package/dist/mcpToolTrust.js +66 -0
  74. package/dist/nativeHook.d.ts +4 -0
  75. package/dist/nativeHook.js +31 -17
  76. package/dist/policyRefresh.d.ts +3 -0
  77. package/dist/policyRefresh.js +27 -0
  78. package/dist/protectionMaintenance.d.ts +10 -0
  79. package/dist/protectionMaintenance.js +64 -0
  80. package/dist/protectionProfile.d.ts +26 -0
  81. package/dist/protectionProfile.js +63 -0
  82. package/dist/runtimeConfig.d.ts +2 -0
  83. package/dist/runtimeConfig.js +65 -26
  84. package/dist/selfTest.d.ts +2 -0
  85. package/dist/selfTest.js +9 -1
  86. package/dist/sessionLimits.d.ts +2 -0
  87. package/dist/sessionLimits.js +24 -0
  88. package/dist/telemetry.d.ts +5 -0
  89. package/dist/telemetry.js +8 -0
  90. package/dist/version.json +1 -1
  91. package/package.json +10 -6
@@ -49,8 +49,11 @@ exports.installGeminiMcpGatewayCommand = installGeminiMcpGatewayCommand;
49
49
  exports.installWindsurfMcpGatewayCommand = installWindsurfMcpGatewayCommand;
50
50
  exports.installVscodeMcpGatewayCommand = installVscodeMcpGatewayCommand;
51
51
  const fs = __importStar(require("fs"));
52
+ const mcpToolTrust_1 = require("../mcpToolTrust");
53
+ const mcpToolReview_1 = require("../mcpToolReview");
52
54
  const os = __importStar(require("os"));
53
55
  const path = __importStar(require("path"));
56
+ const gatewayRuntime_1 = require("../gatewayRuntime");
54
57
  const child_process_1 = require("child_process");
55
58
  const config_1 = require("../config");
56
59
  const discoverPaths_1 = require("./discoverPaths");
@@ -222,6 +225,7 @@ function resolveGatewayConfig(args, config, defaults = {}) {
222
225
  const agentClient = normalizeAgentClient(args.agentClient || process.env.FCD_AGENT_CLIENT || detectRuntimeAgentClient(), defaults.agentClient || 'cursor');
223
226
  const developerName = detectedDeveloperName(args);
224
227
  return {
228
+ mcpServerName: args.mcpServerName,
225
229
  shieldId: creds.shieldId,
226
230
  shieldKey: creds.shieldKey,
227
231
  apiUrl: creds.apiUrl,
@@ -500,19 +504,7 @@ class StdioMcpClient {
500
504
  this.notify('notifications/initialized', {});
501
505
  }
502
506
  async listTools() {
503
- const result = await this.request('tools/list', {});
504
- const tools = result?.tools;
505
- if (!Array.isArray(tools))
506
- return [];
507
- return tools
508
- .filter(tool => typeof tool.name === 'string')
509
- .map(tool => ({
510
- name: String(tool.name),
511
- description: typeof tool.description === 'string'
512
- ? String(tool.description)
513
- : undefined,
514
- inputSchema: tool.inputSchema,
515
- }));
507
+ return (0, mcpToolReview_1.readCompleteToolCatalog)(params => this.request('tools/list', params));
516
508
  }
517
509
  async callTool(name, args) {
518
510
  return this.request('tools/call', { name, arguments: args });
@@ -661,19 +653,7 @@ class HttpMcpClient {
661
653
  throw lastError || new Error(`Could not initialize remote MCP server at ${this.url}`);
662
654
  }
663
655
  async listTools() {
664
- const response = await this.request('tools/list', {});
665
- const tools = response?.tools;
666
- if (!Array.isArray(tools))
667
- return [];
668
- return tools
669
- .filter(tool => typeof tool.name === 'string')
670
- .map(tool => ({
671
- name: String(tool.name),
672
- description: typeof tool.description === 'string'
673
- ? String(tool.description)
674
- : undefined,
675
- inputSchema: tool.inputSchema,
676
- }));
656
+ return (0, mcpToolReview_1.readCompleteToolCatalog)(params => this.request('tools/list', params));
677
657
  }
678
658
  async callTool(name, args) {
679
659
  return this.request('tools/call', { name, arguments: args });
@@ -986,6 +966,8 @@ function describeDeveloperNo(answer) {
986
966
  class McpGatewayServer {
987
967
  gatewayConfig;
988
968
  downstreamSpec;
969
+ toolTrust;
970
+ reportedCatalog = '';
989
971
  downstream;
990
972
  downstreamReady;
991
973
  buffer = '';
@@ -1082,6 +1064,7 @@ class McpGatewayServer {
1082
1064
  if (message.method === 'tools/list') {
1083
1065
  await this.ensureDownstream();
1084
1066
  const tools = await this.downstream.listTools();
1067
+ this.reportCatalog(this.catalog(tools));
1085
1068
  this.respond(message.id, {
1086
1069
  tools: tools.map(tool => ({
1087
1070
  ...tool,
@@ -1117,6 +1100,51 @@ class McpGatewayServer {
1117
1100
  }
1118
1101
  await this.downstreamReady;
1119
1102
  }
1103
+ catalog(tools) {
1104
+ const downstream = this.downstreamSpec.kind === 'http' ? { ...this.downstreamSpec, headers: this.downstreamSpec.headers || {} } : this.downstreamSpec;
1105
+ return (0, mcpToolTrust_1.parseToolCatalog)({ serverId: (0, mcpToolTrust_1.toolDigest)({ server: this.gatewayConfig.mcpServerName || this.gatewayConfig.agentName || 'MCP server', downstream, cwd: process.cwd(), client: this.gatewayConfig.agentClient || '' }), serverName: this.gatewayConfig.mcpServerName || this.gatewayConfig.agentName || 'MCP server', tools });
1106
+ }
1107
+ reportCatalog(catalog) {
1108
+ if (this.reportedCatalog === catalog.catalogHash)
1109
+ return;
1110
+ this.reportedCatalog = catalog.catalogHash;
1111
+ const controller = new AbortController();
1112
+ const timer = setTimeout(() => controller.abort(), 2000);
1113
+ void fetch(`${this.gatewayConfig.apiUrl.replace(/\/$/, '')}/api/cli/tool-catalog`, {
1114
+ method: 'POST', headers: { 'content-type': 'application/json', 'x-shield-id': this.gatewayConfig.shieldId, 'x-shield-key': this.gatewayConfig.shieldKey || '' },
1115
+ body: JSON.stringify({ machineId: (0, machineIdentity_1.getMachineIdentity)().machineId, catalog }), signal: controller.signal,
1116
+ }).then(response => { if (!response.ok)
1117
+ this.reportedCatalog = ''; }).catch(() => { this.reportedCatalog = ''; }).finally(() => clearTimeout(timer));
1118
+ }
1119
+ async checkToolReview(toolName, expectedCatalogHash) {
1120
+ const machineId = (0, machineIdentity_1.getMachineIdentity)().machineId;
1121
+ const bundle = await (0, runtimeConfig_1.getRuntimeBundle)({ apiUrl: this.gatewayConfig.apiUrl, shieldId: this.gatewayConfig.shieldId, shieldKey: this.gatewayConfig.shieldKey, machineId, hotPath: true });
1122
+ try {
1123
+ this.toolTrust ||= new mcpToolReview_1.ToolReviewLease(machineId, this.gatewayConfig.shieldId, this.gatewayConfig.apiUrl);
1124
+ const settings = this.toolTrust.settings(bundle.mcpToolTrust);
1125
+ if (!settings?.enabled)
1126
+ return; // Legacy/unconfigured is not claimed as verified.
1127
+ const catalog = this.catalog(await this.downstream.listTools());
1128
+ this.reportCatalog(catalog);
1129
+ if (expectedCatalogHash && expectedCatalogHash !== catalog.catalogHash)
1130
+ throw new Error('MCP tool definitions changed during this action. Retry so the current version and action are reviewed together.');
1131
+ if (!(0, mcpToolTrust_1.toolCatalogAllowed)(settings, catalog, toolName))
1132
+ throw new Error('This MCP server has new or changed tool definitions. Review this exact version in Action Protection before calling it.');
1133
+ return catalog.catalogHash;
1134
+ }
1135
+ catch (error) {
1136
+ const reason = error instanceof Error ? error.message : 'Tool review is unavailable.';
1137
+ const monitoring = (bundle.source === 'server' || bundle.source === 'cache') && (bundle.mode === 'monitor' || bundle.mode === 'shadow');
1138
+ try {
1139
+ (0, telemetry_1.spoolEvent)({ decision: monitoring ? 'warn' : 'block', toolName, operation: 'mcp:tool-review', reason: monitoring ? `[monitor] ${reason}` : reason, ruleId: 'mcp-tool-review', offlineEnforced: true });
1140
+ (0, telemetry_1.triggerFlush)(!monitoring);
1141
+ }
1142
+ catch { /* Evidence failures cannot change the review decision. */ }
1143
+ if (monitoring)
1144
+ return;
1145
+ throw blockError(reason);
1146
+ }
1147
+ }
1120
1148
  /** Forget a dead/failed downstream so the next call respawns it. */
1121
1149
  resetDownstream(client) {
1122
1150
  if (this.downstream === client) {
@@ -1155,6 +1183,9 @@ class McpGatewayServer {
1155
1183
  const toolName = String(params.name || '');
1156
1184
  if (!toolName)
1157
1185
  throw new Error('Missing MCP tool name.');
1186
+ // Outside the legacy policy-service fail-open recovery: review failures
1187
+ // must never forward a call. Matching metadata still runs every policy.
1188
+ const reviewedCatalogHash = await this.checkToolReview(toolName);
1158
1189
  // What the server receives is untouched (`forwardArgs`). What the policy
1159
1190
  // decision sees is the canonical action: a whole-file write is reduced to
1160
1191
  // the lines it adds so a secret already sitting in the file never blocks
@@ -1196,6 +1227,7 @@ class McpGatewayServer {
1196
1227
  shieldKey: this.gatewayConfig.shieldKey,
1197
1228
  developerName: this.gatewayConfig.developerName,
1198
1229
  machineName: os.hostname(),
1230
+ machineId: (0, machineIdentity_1.getMachineIdentity)().machineId,
1199
1231
  hotPath: true,
1200
1232
  });
1201
1233
  // Admin kill-switch: a suspended machine denies every tool call. This
@@ -1251,7 +1283,7 @@ class McpGatewayServer {
1251
1283
  snapshot.itemActions = Object.fromEntries(Object.entries(snapshot.itemActions || {}).map(([id, action]) => [id, action === 'mask' ? 'warn' : action]));
1252
1284
  snapshot.customBlocks = snapshot.customBlocks.map(rule => rule.action === 'mask' ? { ...rule, action: 'warn' } : rule);
1253
1285
  }
1254
- const requestOutcome = (0, deterministicGuard_1.resolveDeterministicOutcome)(opts => (0, deterministicGuard_1.scanDeterministicToolCall)(toolName, toolArgs, opts), (0, localSafetySnapshot_1.snapshotToScanOptions)(snapshot));
1286
+ const requestOutcome = (0, deterministicGuard_1.resolveDeterministicOutcome)(opts => (0, deterministicGuard_1.scanDeterministicToolCall)(toolName, toolArgs, opts), (0, localSafetySnapshot_1.snapshotToScanOptions)(snapshot, { mcpServerName: this.gatewayConfig.mcpServerName }));
1255
1287
  for (const warning of requestOutcome.warnings) {
1256
1288
  this.spoolLocalFinding(warning, toolName, operation, 'warn');
1257
1289
  process.stderr.write(`FullCourtDefense warning (${warning.itemId}): ${warning.reason}\n`);
@@ -1260,17 +1292,41 @@ class McpGatewayServer {
1260
1292
  // human — through the MCP client (elicitation) when it can show a dialog,
1261
1293
  // otherwise (or when the client answers for the user) through the OS
1262
1294
  // dialog — whenever a human is present. Session memory reuses the IDE
1263
- // hook's ledger (same tool + rule => remembered). Decline, cancel, timeout
1295
+ // hook's ledger (same action + policy + server + session => remembered). Decline, cancel, timeout
1264
1296
  // or no human => block, never a silent allow.
1265
1297
  let localBlock = requestOutcome.blockingFinding || requestOutcome.askFinding;
1266
1298
  let askNote = '';
1267
1299
  if (!requestOutcome.blockingFinding && requestOutcome.askFinding && !reportOnlyMode) {
1268
1300
  const ask = requestOutcome.askFinding;
1269
1301
  const origin = ask.source === 'custom' ? 'org custom rule' : 'built-in Local Safety rule';
1270
- const rememberScope = { toolName, operation: operation || (0, actionPolicyEngine_1.inferToolContext)(toolName, toolArgs).operation, policyId: `local-safety:${ask.itemId}`, matchedRule: ask.ruleId };
1302
+ const rememberScope = {
1303
+ toolName, argsDigest: (0, devConfirm_1.actionDigest)(toolArgs),
1304
+ policyHash: ask.policyHash,
1305
+ authorizationContext: (0, devConfirm_1.actionDigest)({ server: this.gatewayConfig.mcpServerName, downstream: this.downstreamSpec, cwd: process.cwd(), shieldId: this.gatewayConfig.shieldId, client: this.gatewayConfig.agentClient, catalog: reviewedCatalogHash || 'unreviewed' }),
1306
+ operation: operation || (0, actionPolicyEngine_1.inferToolContext)(toolName, toolArgs).operation,
1307
+ policyId: `local-safety:${ask.itemId}`, matchedRule: ask.ruleId,
1308
+ };
1271
1309
  const sessionId = runtimeSessionId() || this.gatewaySessionId;
1310
+ const confirmationEvent = `mcp_gateway:${rememberScope.authorizationContext}:${rememberScope.policyHash}`;
1272
1311
  const remembered = (0, devConfirm_1.isConfirmationRemembered)(rememberScope, sessionId);
1273
- if (remembered) {
1312
+ // Same grant identity the IDE hook uses for this Local Safety item, so one
1313
+ // "Always allow" covers both surfaces.
1314
+ const grantToolName = this.gatewayConfig.mcpServerName ? `mcp__${this.gatewayConfig.mcpServerName}__${toolName}` : toolName;
1315
+ const grantScope = (0, devConfirm_1.grantScopeFor)('mcp', grantToolName, toolArgs, rememberScope.operation, (0, actionPolicyEngine_1.inferToolContext)(toolName, toolArgs).context);
1316
+ const grantRule = { policyId: rememberScope.policyId, policyName: `Local Safety: ${ask.itemId}`, matchedRule: ask.itemId, policyHash: ask.policyHash };
1317
+ const grant = remembered ? undefined : (0, devConfirm_1.findDeveloperGrant)(grantScope, grantRule);
1318
+ if (grant) {
1319
+ (0, telemetry_1.spoolEvent)({
1320
+ decision: 'allow', toolName, operation: rememberScope.operation,
1321
+ reason: `developer granted Always allow for ${(0, devConfirm_1.describeGrantScope)(grant.scope)} — ${ask.reason}`,
1322
+ ruleId: ask.ruleId, category: ask.category, categoryId: ask.categoryId, itemId: ask.itemId, source: ask.source,
1323
+ evidence: ask.evidence, policyHash: ask.policyHash, offlineEnforced: true,
1324
+ approvalScope: 'developer', approvedBy: 'developer',
1325
+ });
1326
+ (0, telemetry_1.triggerFlush)(false);
1327
+ localBlock = undefined;
1328
+ }
1329
+ else if (remembered) {
1274
1330
  (0, telemetry_1.spoolEvent)({
1275
1331
  decision: 'allow', toolName, operation: rememberScope.operation,
1276
1332
  reason: `developer confirmed earlier this session (session memory) — ${ask.reason}`,
@@ -1284,21 +1340,25 @@ class McpGatewayServer {
1284
1340
  else if ((0, devConfirm_1.isHumanPresent)()) {
1285
1341
  const digest = (0, devConfirm_1.actionDigest)(toolArgs);
1286
1342
  const marker = (0, devConfirm_1.raiseDeveloperConfirmation)({
1287
- event: 'mcp_gateway', toolName, operation: rememberScope.operation, argsDigest: digest,
1343
+ event: confirmationEvent, ...rememberScope, argsDigest: digest,
1288
1344
  policyId: rememberScope.policyId, policyName: `Local Safety: ${ask.itemId}`, matchedRule: ask.ruleId,
1289
1345
  sessionId, remember: 'none',
1290
1346
  });
1291
1347
  process.stderr.write(`FullCourtDefense asking the developer to confirm ${toolName}: ${ask.reason} (${origin} "${ask.itemId}", ${ask.ruleId})\n`);
1292
- const answer = await this.confirmWithDeveloper({ toolName, reason: ask.reason, ruleLabel: `${origin} "${ask.itemId}" (${ask.ruleId})`, evidence: ask.evidence, argsDigest: digest });
1348
+ const alwaysScopeLabel = (0, devConfirm_1.describeGrantScope)(grantScope);
1349
+ const answer = await this.confirmWithDeveloper({ toolName, reason: ask.reason, ruleLabel: `${origin} "${ask.itemId}" (${ask.ruleId})`, evidence: ask.evidence, argsDigest: digest, allowAlways: true, alwaysScopeLabel });
1293
1350
  if (answer.answer === 'accept') {
1294
1351
  // Settle through the shared ledger so "remember for this session" is
1295
1352
  // recorded exactly like an IDE approval would be.
1296
1353
  if (answer.remember)
1297
1354
  (0, devConfirm_1.raiseDeveloperConfirmation)({ ...marker, remember: 'session' });
1298
- (0, devConfirm_1.settleDeveloperConfirmation)('mcp_gateway', toolName, digest);
1355
+ (0, devConfirm_1.settleDeveloperConfirmation)(confirmationEvent, toolName, digest);
1356
+ const granted = answer.always && ask.policyHash ? (0, devConfirm_1.recordDeveloperGrant)(grantScope, { ...grantRule, policyHash: ask.policyHash }) : undefined;
1299
1357
  (0, telemetry_1.spoolEvent)({
1300
1358
  decision: 'allow', toolName, operation: rememberScope.operation,
1301
- reason: `developer confirmed in ${confirmChannelLabel(answer)}${answer.remember ? ' (remembered for this session)' : ''}${answer.clientAutoAnswered ? ' (the MCP client auto-declined the in-client prompt)' : ''} — ${ask.reason}`,
1359
+ reason: granted
1360
+ ? `developer chose Always allow in ${confirmChannelLabel(answer)} — ${alwaysScopeLabel} (30 days, this rule) — ${ask.reason}`
1361
+ : `developer confirmed in ${confirmChannelLabel(answer)}${answer.remember ? ' (remembered for this session)' : ''}${answer.clientAutoAnswered ? ' (the MCP client auto-declined the in-client prompt)' : ''} — ${ask.reason}`,
1302
1362
  ruleId: ask.ruleId, category: ask.category, categoryId: ask.categoryId, itemId: ask.itemId, source: ask.source,
1303
1363
  evidence: ask.evidence, policyHash: ask.policyHash, offlineEnforced: true,
1304
1364
  approvalScope: 'developer', approvedBy: 'developer', confirmLatencyMs: answer.latencyMs,
@@ -1307,7 +1367,7 @@ class McpGatewayServer {
1307
1367
  localBlock = undefined;
1308
1368
  }
1309
1369
  else {
1310
- (0, devConfirm_1.rejectDeveloperConfirmation)('mcp_gateway', toolName, digest);
1370
+ (0, devConfirm_1.rejectDeveloperConfirmation)(confirmationEvent, toolName, digest);
1311
1371
  const no = describeDeveloperNo(answer);
1312
1372
  (0, telemetry_1.spoolEvent)({
1313
1373
  decision: 'block', toolName, operation: rememberScope.operation,
@@ -1601,6 +1661,7 @@ class McpGatewayServer {
1601
1661
  }
1602
1662
  }
1603
1663
  downstreamAttempted = true;
1664
+ await this.checkToolReview(toolName, reviewedCatalogHash);
1604
1665
  let rawResult = await this.downstream.callTool(toolName, forwardArgs);
1605
1666
  downstreamRan = true;
1606
1667
  downstreamResult = rawResult;
@@ -1793,6 +1854,7 @@ class McpGatewayServer {
1793
1854
  if (!isBlockError(error) && !downstreamAttempted) {
1794
1855
  // Pre-call bug: forward the tool call now (exactly once).
1795
1856
  try {
1857
+ await this.checkToolReview(toolName, reviewedCatalogHash);
1796
1858
  const recovered = await this.downstream.callTool(toolName, forwardArgs);
1797
1859
  try {
1798
1860
  (0, telemetry_1.spoolEvent)({ decision: 'allow', toolName, operation, reason: `gateway error failed open (pre-call): ${message.slice(0, 200)}`, offlineEnforced: true });
@@ -1878,8 +1940,13 @@ class McpGatewayServer {
1878
1940
  (0, telemetry_1.triggerFlush)(true);
1879
1941
  throw blockError(reason);
1880
1942
  }
1881
- const rememberScope = { toolName, operation, policyId: verdict.policyId, matchedRule: verdict.matchedRule };
1943
+ const rememberScope = {
1944
+ toolName, operation, argsDigest: (0, devConfirm_1.actionDigest)(toolArgs),
1945
+ policyId: verdict.policyId, matchedRule: verdict.matchedRule, policyHash: (0, devConfirm_1.actionDigest)({ policies }),
1946
+ authorizationContext: (0, devConfirm_1.actionDigest)({ server: this.gatewayConfig.mcpServerName, downstream: this.downstreamSpec, cwd: process.cwd(), shieldId: this.gatewayConfig.shieldId, client: this.gatewayConfig.agentClient }),
1947
+ };
1882
1948
  const sessionId = runtimeSessionId() || this.gatewaySessionId;
1949
+ const confirmationEvent = `mcp_gateway:${rememberScope.authorizationContext}:${rememberScope.policyHash}`;
1883
1950
  const policyLabel = verdict.policyName ? ` (${verdict.policyName})` : '';
1884
1951
  const ruleId = verdict.policyName ? `action-policy:${verdict.policyName}` : undefined;
1885
1952
  const remembered = verdict.approval?.remember === 'none' ? undefined : (0, devConfirm_1.isConfirmationRemembered)(rememberScope, sessionId);
@@ -1894,8 +1961,24 @@ class McpGatewayServer {
1894
1961
  }
1895
1962
  const digest = (0, devConfirm_1.actionDigest)(toolArgs);
1896
1963
  const remember = verdict.approval?.remember ?? 'none';
1964
+ // A standing "Always allow" from the FullCourtDefense dialog for this server + tool
1965
+ // + operation (+ destination) under this exact rule. Rules with `remember: none`
1966
+ // never offer it and never honour one.
1967
+ const grantToolName = this.gatewayConfig.mcpServerName ? `mcp__${this.gatewayConfig.mcpServerName}__${toolName}` : toolName;
1968
+ const grantScope = (0, devConfirm_1.grantScopeFor)('mcp', grantToolName, toolArgs, operation, (0, actionPolicyEngine_1.inferToolContext)(toolName, toolArgs).context);
1969
+ const grantRule = { policyId: verdict.policyId, policyName: verdict.policyName, matchedRule: verdict.matchedRule, policyHash: rememberScope.policyHash };
1970
+ const grant = remember === 'none' ? undefined : (0, devConfirm_1.findDeveloperGrant)(grantScope, grantRule);
1971
+ if (grant) {
1972
+ (0, telemetry_1.spoolEvent)({
1973
+ decision: 'allow', toolName, operation,
1974
+ reason: `developer granted Always allow for ${(0, devConfirm_1.describeGrantScope)(grant.scope)}${policyLabel}`,
1975
+ ruleId, offlineEnforced: true, approvalScope: 'developer', approvedBy: 'developer',
1976
+ });
1977
+ (0, telemetry_1.triggerFlush)(false);
1978
+ return true;
1979
+ }
1897
1980
  const marker = (0, devConfirm_1.raiseDeveloperConfirmation)({
1898
- event: 'mcp_gateway', toolName, operation, argsDigest: digest,
1981
+ event: confirmationEvent, ...rememberScope, argsDigest: digest,
1899
1982
  policyId: verdict.policyId, policyName: verdict.policyName, matchedRule: verdict.matchedRule,
1900
1983
  sessionId, remember,
1901
1984
  });
@@ -1909,25 +1992,31 @@ class McpGatewayServer {
1909
1992
  ? ' This policy is set to Block; on a developer workstation it asks you instead. An admin can mark it "Hard block" in the console to stop this without asking.'
1910
1993
  : '';
1911
1994
  process.stderr.write(`FullCourtDefense asking the developer to confirm ${toolName}${policyLabel}: ${verdict.reason || verdict.matchedRule || 'action policy'}\n`);
1995
+ const alwaysScopeLabel = (0, devConfirm_1.describeGrantScope)(grantScope);
1912
1996
  const answer = await this.confirmWithDeveloper({
1913
1997
  toolName,
1914
1998
  reason: `${verdict.reason || `${toolName} needs your confirmation`}.${ceiling}`,
1915
1999
  allowRemember: false,
1916
2000
  ruleLabel: verdict.policyName ? `Action Policy "${verdict.policyName}"${verdict.matchedRule ? ` — ${verdict.matchedRule}` : ''}` : 'Action Policy',
1917
2001
  argsDigest: digest,
2002
+ allowAlways: remember !== 'none',
2003
+ alwaysScopeLabel,
1918
2004
  });
1919
2005
  if (answer.answer === 'accept') {
1920
2006
  // Policy memory limits are authoritative; a client cannot upgrade 'none'.
1921
- (0, devConfirm_1.settleDeveloperConfirmation)('mcp_gateway', toolName, digest);
2007
+ (0, devConfirm_1.settleDeveloperConfirmation)(confirmationEvent, toolName, digest);
2008
+ const granted = answer.always && remember !== 'none' ? (0, devConfirm_1.recordDeveloperGrant)(grantScope, grantRule) : undefined;
1922
2009
  (0, telemetry_1.spoolEvent)({
1923
2010
  decision: 'allow', toolName, operation,
1924
- reason: `developer confirmed in ${confirmChannelLabel(answer)}${answer.remember ? ' (remembered for this session)' : ''}${answer.clientAutoAnswered ? ' (the MCP client auto-declined the in-client prompt)' : ''}${policyLabel}`,
2011
+ reason: granted
2012
+ ? `developer chose Always allow in ${confirmChannelLabel(answer)} — ${alwaysScopeLabel} (30 days, this rule)${policyLabel}`
2013
+ : `developer confirmed in ${confirmChannelLabel(answer)}${answer.remember ? ' (remembered for this session)' : ''}${answer.clientAutoAnswered ? ' (the MCP client auto-declined the in-client prompt)' : ''}${policyLabel}`,
1925
2014
  ruleId, offlineEnforced: true, approvalScope: 'developer', approvedBy: 'developer', confirmLatencyMs: answer.latencyMs,
1926
2015
  });
1927
2016
  (0, telemetry_1.triggerFlush)(false);
1928
2017
  return true;
1929
2018
  }
1930
- (0, devConfirm_1.rejectDeveloperConfirmation)('mcp_gateway', toolName, digest);
2019
+ (0, devConfirm_1.rejectDeveloperConfirmation)(confirmationEvent, toolName, digest);
1931
2020
  const no = describeDeveloperNo(answer);
1932
2021
  (0, telemetry_1.spoolEvent)({
1933
2022
  decision: 'block', toolName, operation,
@@ -1961,7 +2050,8 @@ class McpGatewayServer {
1961
2050
  async confirmWithDeveloper(input) {
1962
2051
  if (!(0, devConfirm_1.isHumanPresent)())
1963
2052
  return { answer: 'unavailable', remember: false, latencyMs: 0, clientAutoAnswered: false };
1964
- const handoff = input.argsDigest ? (0, devConfirm_1.consumeConfirmedAction)(input.toolName, input.argsDigest) : undefined;
2053
+ const handoff = input.argsDigest && this.gatewayConfig.mcpServerName
2054
+ ? (0, devConfirm_1.consumeConfirmedAction)(`mcp__${this.gatewayConfig.mcpServerName}__${input.toolName}`, input.argsDigest) : undefined;
1965
2055
  if (handoff) {
1966
2056
  return { answer: 'accept', remember: false, latencyMs: Math.max(0, Date.now() - Date.parse(handoff.confirmedAt)), channel: 'hook_dialog', clientAutoAnswered: false };
1967
2057
  }
@@ -1988,10 +2078,10 @@ class McpGatewayServer {
1988
2078
  }
1989
2079
  async askInOsDialog(input, clientAutoAnswered) {
1990
2080
  const evidence = input.evidence ? `\nEvidence: ${input.evidence.slice(0, 240)}` : '';
1991
- const dialog = await (0, devConfirm_1.confirmNativeActionDetailed)(`Agent: ${this.gatewayConfig.agentName}\nIDE: ${this.gatewayConfig.agentClient}\nDeveloper: ${this.gatewayConfig.developerName || 'Unknown'}\nTool: ${input.toolName}\nRule: ${input.ruleLabel}\nWhy: ${input.reason}${evidence}\n\nYour answer is reported to your security dashboard as your decision.`, OS_DIALOG_TIMEOUT_MS);
2081
+ const dialog = await (0, devConfirm_1.confirmNativeActionDetailed)(`Agent: ${this.gatewayConfig.agentName}\nIDE: ${this.gatewayConfig.agentClient}\nDeveloper: ${this.gatewayConfig.developerName || 'Unknown'}\nTool: ${input.toolName}\nRule: ${input.ruleLabel}\nWhy: ${input.reason}${evidence}\n\nYour answer is reported to your security dashboard as your decision.`, OS_DIALOG_TIMEOUT_MS, { allowAlways: input.allowAlways === true, alwaysScopeLabel: input.alwaysScopeLabel });
1992
2082
  process.stderr.write(`FullCourtDefense: dialog for ${input.toolName} ended with "${dialog.outcome}" after ${dialog.latencyMs}ms.\n`);
1993
- const answer = dialog.outcome === 'allow' ? 'accept' : dialog.outcome === 'deny' ? 'decline' : dialog.outcome;
1994
- return { answer, remember: false, latencyMs: dialog.latencyMs, channel: 'os_dialog', clientAutoAnswered };
2083
+ const answer = dialog.outcome === 'allow' || dialog.outcome === 'allow_always' ? 'accept' : dialog.outcome === 'deny' ? 'decline' : dialog.outcome;
2084
+ return { answer, remember: false, always: dialog.outcome === 'allow_always', latencyMs: dialog.latencyMs, channel: 'os_dialog', clientAutoAnswered };
1995
2085
  }
1996
2086
  async elicitDeveloperConfirmation(input) {
1997
2087
  const started = Date.now();
@@ -2010,7 +2100,7 @@ class McpGatewayServer {
2010
2100
  ...(input.allowRemember === false ? {} : { remember: {
2011
2101
  type: 'boolean',
2012
2102
  title: 'Remember for this session',
2013
- description: 'Do not ask again for this tool and rule until this MCP session ends (max 12 hours).',
2103
+ description: 'Remember this exact action in this session (max 12 hours). Changed arguments, server or policy require a new approval.',
2014
2104
  default: false,
2015
2105
  } }),
2016
2106
  },
@@ -2083,6 +2173,9 @@ function readJson(file) {
2083
2173
  function gatewayScriptPath() {
2084
2174
  return path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
2085
2175
  }
2176
+ function gatewayRuntime(entry) {
2177
+ return (0, gatewayRuntime_1.resolveGatewayRuntime)({ executable: process.execPath, entry: gatewayScriptPath() }, entry);
2178
+ }
2086
2179
  /**
2087
2180
  * Resolve which real MCP server an installer should protect: a local stdio
2088
2181
  * command (--mcp-command/--mcp-args) or a remote HTTP/SSE URL (--mcp-url,
@@ -2115,6 +2208,7 @@ function buildGatewayCommandArgs(gatewayConfig, downstream, options = {}) {
2115
2208
  gatewayScriptPath(),
2116
2209
  'mcp-gateway',
2117
2210
  ...downstreamFlags,
2211
+ ...(gatewayConfig.mcpServerName ? ['--mcp-server-name', gatewayConfig.mcpServerName] : []),
2118
2212
  '--agent-name', gatewayConfig.agentName,
2119
2213
  '--agent-client', gatewayConfig.agentClient,
2120
2214
  '--developer-name', gatewayConfig.developerName,
@@ -2477,9 +2571,10 @@ function isGatewayWrappedEntry(entry) {
2477
2571
  function wrappedEntryNeedsHeal(entry) {
2478
2572
  const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
2479
2573
  const credentialPairStale = args.includes('--shield-id') && !args.includes('--shield-key');
2480
- const runtimePathStale = (0, discoverPaths_1.normalizedPath)(String(entry?.command || '')) !== (0, discoverPaths_1.normalizedPath)(process.execPath)
2574
+ const runtime = gatewayRuntime(entry);
2575
+ const runtimePathStale = (0, discoverPaths_1.normalizedPath)(String(entry?.command || '')) !== (0, discoverPaths_1.normalizedPath)(runtime.executable)
2481
2576
  || !args[0]
2482
- || (0, discoverPaths_1.normalizedPath)(args[0]) !== (0, discoverPaths_1.normalizedPath)(gatewayScriptPath());
2577
+ || (0, discoverPaths_1.normalizedPath)(args[0]) !== (0, discoverPaths_1.normalizedPath)(runtime.entry);
2483
2578
  return credentialPairStale || runtimePathStale;
2484
2579
  }
2485
2580
  /** Recover the original downstream server (stdio command or remote URL) from a wrapped entry's args. */
@@ -2745,6 +2840,7 @@ function healCodexTomlAgentIdentities(file, dryRun) {
2745
2840
  * entry reported `boazl-claude-desktop`. An explicit --agent-name / FCD_AGENT_NAME still wins.
2746
2841
  */
2747
2842
  function applyPerServerAgentName(gatewayConfig, args, serverName) {
2843
+ gatewayConfig = { ...gatewayConfig, mcpServerName: serverName };
2748
2844
  if (args.agentName || process.env.FCD_AGENT_NAME)
2749
2845
  return gatewayConfig;
2750
2846
  if (!serverName || serverName === MANAGED_SERVER_NAME)
@@ -2766,6 +2862,7 @@ function perServerGatewayConfig(gatewayConfig, agentClient, serverName) {
2766
2862
  return {
2767
2863
  ...gatewayConfig,
2768
2864
  agentClient,
2865
+ mcpServerName: serverName,
2769
2866
  agentName: perServerAgentName(gatewayConfig.developerName, agentClient, serverName),
2770
2867
  userObjective: gatewayConfig.userObjectiveExplicit ? gatewayConfig.userObjective : defaultUserObjective(agentClient),
2771
2868
  };
@@ -2801,12 +2898,14 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
2801
2898
  const downstream = credentialPairStale ? extractFullyUnwrappedDownstream(entry) : null;
2802
2899
  if (!credentialPairStale || downstream) {
2803
2900
  const perServer = perServerGatewayConfig(gatewayConfig, agentClient, name);
2804
- entry.command = nodeExe;
2901
+ const runtime = gatewayRuntime(entry);
2902
+ entry.command = runtime.executable;
2805
2903
  // Path-only upgrades preserve every downstream/config flag exactly.
2806
2904
  // The credential-pair bug requires a safe rebuild from downstream.
2807
- entry.args = credentialPairStale
2905
+ const repairedArgs = credentialPairStale
2808
2906
  ? buildGatewayCommandArgs(perServer, downstream, INSTALL_GATEWAY_ARGS)
2809
- : [gatewayScriptPath(), ...existingArgs.slice(1)];
2907
+ : existingArgs;
2908
+ entry.args = [runtime.entry, ...repairedArgs.slice(1)];
2810
2909
  stats.healed.push(name);
2811
2910
  changed = true;
2812
2911
  continue;
@@ -3010,13 +3109,21 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
3010
3109
  }
3011
3110
  if (wrappedAlready) {
3012
3111
  const existingArgs = section.args || [];
3013
- const pathStale = (0, discoverPaths_1.normalizedPath)(section.command) !== (0, discoverPaths_1.normalizedPath)(nodeExe)
3014
- || !existingArgs[0]
3015
- || (0, discoverPaths_1.normalizedPath)(existingArgs[0]) !== (0, discoverPaths_1.normalizedPath)(gatewayScriptPath());
3016
- if (pathStale) {
3112
+ const entry = { command: section.command, args: existingArgs };
3113
+ const runtime = gatewayRuntime(entry);
3114
+ const credentialPairStale = existingArgs.includes('--shield-id') && !existingArgs.includes('--shield-key');
3115
+ if (wrappedEntryNeedsHeal(entry)) {
3116
+ const downstream = credentialPairStale ? extractFullyUnwrappedDownstream(entry) : null;
3117
+ // An invalid wrapper cannot be reconstructed without its downstream.
3118
+ // Leave it visible as unhealthy instead of manufacturing a success.
3119
+ if (credentialPairStale && !downstream)
3120
+ continue;
3017
3121
  const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
3018
- lines[section.cmdLine] = `${indent}command = ${JSON.stringify(nodeExe)}`;
3019
- const repairedArgs = [gatewayScriptPath(), ...existingArgs.slice(1)];
3122
+ lines[section.cmdLine] = `${indent}command = ${JSON.stringify(runtime.executable)}`;
3123
+ const args = credentialPairStale
3124
+ ? buildGatewayCommandArgs(perServerGatewayConfig(gatewayConfig, 'codex', section.name), downstream, INSTALL_GATEWAY_ARGS)
3125
+ : existingArgs;
3126
+ const repairedArgs = [runtime.entry, ...args.slice(1)];
3020
3127
  const argsLineText = `${indent}args = ${tomlStringArrayLiteral(repairedArgs)}`;
3021
3128
  if (section.argsLine !== -1)
3022
3129
  lines[section.argsLine] = argsLineText;
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.onboardCommand = onboardCommand;
37
37
  const child_process_1 = require("child_process");
38
+ const onboardingSummary_1 = require("./onboardingSummary");
38
39
  const fs = __importStar(require("fs"));
39
40
  const os = __importStar(require("os"));
40
41
  const path = __importStar(require("path"));
@@ -669,15 +670,15 @@ async function onboardCommand(args, config) {
669
670
  // already open still run their OLD (unwrapped) configuration until they
670
671
  // restart. Never print an unconditional green "protected" before that.
671
672
  const runningClients = dryRun ? [] : detectRunningAiClients();
672
- console.log(`${GREEN}${BOLD}Protection is installed and verified.${RESET} This machine reports to your org's AI Fleet in monitor mode — actions are recorded and analyzed while the fleet baselines this machine. An admin can promote it to blocking mode from the console.`);
673
- if (runningClients.length > 0) {
673
+ console.log(`${GREEN}${BOLD}${(0, onboardingSummary_1.onboardingVerificationSummary)({ dryRun, firstHeartbeatOk })}${RESET}`);
674
+ if (!dryRun && runningClients.length > 0) {
674
675
  console.log('');
675
676
  console.log(`${YELLOW}${BOLD} ! ACTION REQUIRED — restart ${runningClients.join(', ')}.${RESET}`);
676
- console.log(`${YELLOW} ${runningClients.length === 1 ? 'This app is' : 'These apps are'} running with the configuration from before onboarding — agent actions inside ${runningClients.length === 1 ? 'it are' : 'them are'} NOT protected until restarted. Anything launched from now on is protected automatically.${RESET}`);
677
+ console.log(`${YELLOW} Restart to load the installed integrations, then check reported coverage and a test action in the console.${RESET}`);
677
678
  }
678
- else {
679
+ else if (!dryRun) {
679
680
  console.log('');
680
- console.log(`${YELLOW}${BOLD} ! One step left:${RESET} ${YELLOW}restart any AI apps that were open during onboarding (Cursor, Claude, VS Code, …) so they load the protected configuration. Apps launched from now on are protected automatically.${RESET}`);
681
+ console.log(`${YELLOW}${BOLD} ! One step left:${RESET} ${YELLOW}restart AI apps that were open during onboarding so they load the installed integrations. Check reported coverage and a test action in the console.${RESET}`);
681
682
  }
682
683
  console.log('');
683
684
  console.log(`${DIM}Fleet view: ${dashboard}${RESET}`);
@@ -0,0 +1,5 @@
1
+ /** Describe only the installation and delivery evidence collected by onboard. */
2
+ export declare function onboardingVerificationSummary(input: {
3
+ dryRun: boolean;
4
+ firstHeartbeatOk: boolean;
5
+ }): string;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onboardingVerificationSummary = onboardingVerificationSummary;
4
+ /** Describe only the installation and delivery evidence collected by onboard. */
5
+ function onboardingVerificationSummary(input) {
6
+ if (input.dryRun)
7
+ return 'Dry-run checks passed. Installation and reporting were not verified.';
8
+ return 'Installation checks passed. ' + (input.firstHeartbeatOk
9
+ ? 'The console received the first machine report. Check Policies for this machine’s current enforcement mode.'
10
+ : 'The first machine report was not delivered. Check the connection and retry; console visibility is not yet confirmed.');
11
+ }
@@ -0,0 +1,14 @@
1
+ import { BotGuardConfig } from '../config';
2
+ import { VerifiableMachineAction } from '../machineActionVerify';
3
+ import { ProtectionProfile } from '../protectionProfile';
4
+ export declare function verifiedProtectionProfile(envelope: VerifiableMachineAction | null | undefined, machineId: string, shieldId: string): ProtectionProfile;
5
+ export declare function protectedRunCommand(config: BotGuardConfig, command: string | undefined, inspect?: boolean): Promise<void>;
6
+ /** Existing machine Activity receives minimized launch outcome evidence. */
7
+ export declare class ContainedLaunchFailure extends Error {
8
+ readonly executionStarted: boolean;
9
+ constructor(message: string, executionStarted: boolean);
10
+ }
11
+ export declare function runProtectedLaunch(profile: ProtectionProfile, command: string, revision: string, launch?: typeof runContainedCommand): Promise<number>;
12
+ export declare function containedExecutionEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
13
+ /** Runtime adapter, shared with real-process OS acceptance tests. */
14
+ export declare function runContainedCommand(value: ProtectionProfile, command: string): Promise<number>;