fullcourtdefense-cli 1.32.0 → 1.34.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/dist/actionPolicyEngine.js +18 -2
- package/dist/agentTerminal.d.ts +6 -3
- package/dist/agentTerminal.js +19 -16
- package/dist/commands/cmdGuard.js +2 -2
- package/dist/commands/deterministicGuard.d.ts +2 -0
- package/dist/commands/deterministicGuard.js +17 -4
- package/dist/commands/discoverPaths.js +2 -0
- package/dist/commands/hook.d.ts +1 -0
- package/dist/commands/hook.js +34 -19
- package/dist/commands/installAll.js +13 -2
- package/dist/commands/installClaudeHook.d.ts +9 -3
- package/dist/commands/installClaudeHook.js +48 -2
- package/dist/commands/installNativeHooks.d.ts +9 -0
- package/dist/commands/installNativeHooks.js +145 -0
- package/dist/commands/mcpGateway.js +48 -41
- package/dist/commands/posixShellGuard.js +2 -2
- package/dist/commands/shellGuard.js +57 -14
- package/dist/commands/uninstallAll.js +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -0
- package/dist/devConfirm.d.ts +4 -0
- package/dist/devConfirm.js +15 -4
- package/dist/hookIo.js +1 -0
- package/dist/index.js +21 -0
- package/dist/nativeHook.d.ts +25 -0
- package/dist/nativeHook.js +239 -0
- package/dist/runtimeConfig.d.ts +5 -0
- package/dist/runtimeConfig.js +6 -0
- package/dist/telemetry.d.ts +3 -0
- package/dist/telemetry.js +7 -0
- package/dist/version.json +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.nativeHookCommand = nativeHookCommand;
|
|
37
|
+
exports.installNativeHooks = installNativeHooks;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const marker = '--fcd-native-managed true';
|
|
42
|
+
const managed = (value) => {
|
|
43
|
+
if (typeof value?.command !== 'string')
|
|
44
|
+
return false;
|
|
45
|
+
if (value.command.includes(marker))
|
|
46
|
+
return true;
|
|
47
|
+
const encoded = /-EncodedCommand ([A-Za-z0-9+/=]+)$/.exec(value.command)?.[1];
|
|
48
|
+
return Boolean(encoded && Buffer.from(encoded, 'base64').toString('utf16le').includes(marker));
|
|
49
|
+
};
|
|
50
|
+
const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
51
|
+
function nativeHookCommand(client) {
|
|
52
|
+
if (!['codex', 'gemini_cli', 'windsurf', 'kiro'].includes(client))
|
|
53
|
+
throw new Error('Unsupported native hook client');
|
|
54
|
+
const entry = path.resolve(__dirname, '..', 'nativeHook.js');
|
|
55
|
+
const flags = `--agent-client ${client} ${marker} --fcd-managed true --local-only true`;
|
|
56
|
+
if (process.platform !== 'win32')
|
|
57
|
+
return `${quote(process.execPath)} ${quote(entry)} ${flags}`;
|
|
58
|
+
// Windows vendors use different shells. An encoded PowerShell launcher is
|
|
59
|
+
// valid in both cmd and PowerShell, preserves paths with spaces, and forwards
|
|
60
|
+
// the JSON stdin explicitly. Paths are data, never interpolated expressions.
|
|
61
|
+
const psQuote = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
62
|
+
const script = `$ProgressPreference='SilentlyContinue'; $OutputEncoding=[Text.UTF8Encoding]::new($false); [Console]::InputEncoding=$OutputEncoding; [Console]::OutputEncoding=$OutputEncoding; $raw=[Console]::In.ReadToEnd(); $raw | & ${psQuote(process.execPath)} ${psQuote(entry)} ${flags}; exit $LASTEXITCODE`;
|
|
63
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, 'utf16le').toString('base64')}`;
|
|
64
|
+
}
|
|
65
|
+
/** Preserve unrelated settings and hooks; refuse malformed files. */
|
|
66
|
+
function installNativeHooks(options = {}) {
|
|
67
|
+
// Validate library callers too, before any filesystem changes.
|
|
68
|
+
for (const client of options.clients || [])
|
|
69
|
+
nativeHookCommand(client);
|
|
70
|
+
const home = options.home || os.homedir();
|
|
71
|
+
const files = [];
|
|
72
|
+
const errors = [];
|
|
73
|
+
for (const client of options.clients || ['codex', 'gemini_cli', 'windsurf', 'kiro']) {
|
|
74
|
+
const root = client === 'codex' ? path.join(home, '.codex') : client === 'gemini_cli' ? path.join(home, '.gemini')
|
|
75
|
+
: client === 'windsurf' ? path.join(home, '.codeium', 'windsurf') : path.join(options.project || process.cwd(), '.kiro');
|
|
76
|
+
// Install only detected clients. Explicit client selection also supports setup before first launch.
|
|
77
|
+
if (!options.clients && !fs.existsSync(root))
|
|
78
|
+
continue;
|
|
79
|
+
const file = path.join(root, client === 'kiro' ? 'hooks/fullcourtdefense.json' : client === 'gemini_cli' ? 'settings.json' : 'hooks.json');
|
|
80
|
+
if (options.remove && !fs.existsSync(file))
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const data = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
|
|
84
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
85
|
+
throw new Error(`Invalid hook settings: ${file}`);
|
|
86
|
+
const before = JSON.stringify(data);
|
|
87
|
+
const command = nativeHookCommand(client);
|
|
88
|
+
if (client === 'kiro') {
|
|
89
|
+
if (data.hooks !== undefined && !Array.isArray(data.hooks))
|
|
90
|
+
throw new Error(`Invalid Kiro hooks: ${file}`);
|
|
91
|
+
const kept = (data.hooks || []).filter((hook) => !managed(hook.action));
|
|
92
|
+
data.version = 'v1';
|
|
93
|
+
data.hooks = options.remove ? kept : [...kept, ...['PreToolUse', 'PostToolUse', 'UserPromptSubmit'].map(trigger => ({
|
|
94
|
+
name: `FullCourtDefense ${trigger}`, trigger, action: { type: 'command', command }, timeout: 130,
|
|
95
|
+
}))];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (data.hooks !== undefined && (!data.hooks || typeof data.hooks !== 'object' || Array.isArray(data.hooks)))
|
|
99
|
+
throw new Error(`Invalid hooks: ${file}`);
|
|
100
|
+
const hooks = data.hooks || {};
|
|
101
|
+
const events = client === 'windsurf'
|
|
102
|
+
? ['pre_run_command', 'post_run_command', 'pre_read_code', 'pre_write_code', 'post_write_code', 'pre_mcp_tool_use', 'post_mcp_tool_use', 'pre_user_prompt']
|
|
103
|
+
: client === 'gemini_cli' ? ['BeforeTool', 'AfterTool', 'BeforeAgent'] : ['PreToolUse', 'PostToolUse', 'UserPromptSubmit'];
|
|
104
|
+
for (const event of events) {
|
|
105
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event]))
|
|
106
|
+
throw new Error(`Invalid ${event} hooks: ${file}`);
|
|
107
|
+
const entries = hooks[event] || [];
|
|
108
|
+
const kept = client === 'windsurf' ? entries.filter((hook) => !managed(hook)) : entries.flatMap((group) => {
|
|
109
|
+
if (!Array.isArray(group?.hooks))
|
|
110
|
+
throw new Error(`Invalid ${event} hook group: ${file}`);
|
|
111
|
+
const rest = group.hooks.filter((hook) => !managed(hook));
|
|
112
|
+
return rest.length ? [{ ...group, hooks: rest }] : [];
|
|
113
|
+
});
|
|
114
|
+
if (!options.remove) {
|
|
115
|
+
if (client === 'windsurf')
|
|
116
|
+
kept.push({ command, powershell: `${command}; exit $LASTEXITCODE`, show_output: true });
|
|
117
|
+
else
|
|
118
|
+
kept.push({ ...(event.includes('Tool') ? { matcher: '.*' } : {}), hooks: [{ type: 'command', command,
|
|
119
|
+
...(client === 'gemini_cli' ? { name: `fullcourtdefense-${event}`, timeout: 130000 } : { timeout: 130 }) }] });
|
|
120
|
+
}
|
|
121
|
+
if (kept.length)
|
|
122
|
+
hooks[event] = kept;
|
|
123
|
+
else
|
|
124
|
+
delete hooks[event];
|
|
125
|
+
}
|
|
126
|
+
data.hooks = hooks;
|
|
127
|
+
}
|
|
128
|
+
if (JSON.stringify(data) !== before) {
|
|
129
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
130
|
+
if (fs.existsSync(file) && !fs.existsSync(`${file}.fcd-backup`))
|
|
131
|
+
fs.copyFileSync(file, `${file}.fcd-backup`);
|
|
132
|
+
const temporary = `${file}.fcd-${process.pid}.tmp`;
|
|
133
|
+
fs.writeFileSync(temporary, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
134
|
+
fs.renameSync(temporary, file);
|
|
135
|
+
}
|
|
136
|
+
files.push(file);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
errors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (errors.length)
|
|
143
|
+
throw new Error(`Native hook settings need repair: ${errors.join('; ')}`);
|
|
144
|
+
return files;
|
|
145
|
+
}
|
|
@@ -1086,7 +1086,7 @@ class McpGatewayServer {
|
|
|
1086
1086
|
// waited for OUR decision (path 'gateway'). Pure in-memory context — no
|
|
1087
1087
|
// probe, no I/O; the clock is restarted after the downstream call so the
|
|
1088
1088
|
// tool's own runtime is never billed to FullCourtDefense.
|
|
1089
|
-
return (0, telemetry_1.markVerdictTiming)({ startedAt: Date.now(), path: 'gateway' }, () => this.handleToolCallInner(params));
|
|
1089
|
+
return (0, telemetry_1.markVerdictTiming)({ startedAt: Date.now(), path: 'gateway', agentClient: this.gatewayConfig.agentClient }, () => this.handleToolCallInner(params));
|
|
1090
1090
|
}
|
|
1091
1091
|
async handleToolCallInner(params) {
|
|
1092
1092
|
await this.ensureDownstream();
|
|
@@ -1109,6 +1109,8 @@ class McpGatewayServer {
|
|
|
1109
1109
|
let downstreamResult;
|
|
1110
1110
|
let downstreamAttempted = false;
|
|
1111
1111
|
let downstreamRan = false;
|
|
1112
|
+
let reportOnlyMode = true;
|
|
1113
|
+
let policyWarning;
|
|
1112
1114
|
try {
|
|
1113
1115
|
// The runtime bundle carries the org's current Local Safety policy hash —
|
|
1114
1116
|
// passing it as `expectedPolicyHash` makes a console policy edit take
|
|
@@ -1123,7 +1125,7 @@ class McpGatewayServer {
|
|
|
1123
1125
|
let cachedPolicies;
|
|
1124
1126
|
// Monitor unless a server/cache bundle explicitly says 'block': a machine
|
|
1125
1127
|
// with no signal (fresh install + offline, wiped cache) must never block.
|
|
1126
|
-
|
|
1128
|
+
reportOnlyMode = true;
|
|
1127
1129
|
let machineRole;
|
|
1128
1130
|
try {
|
|
1129
1131
|
const bundle = await (0, runtimeConfig_1.getRuntimeBundle)({
|
|
@@ -1183,6 +1185,10 @@ class McpGatewayServer {
|
|
|
1183
1185
|
expectedPolicyHash,
|
|
1184
1186
|
preferCached: reportOnlyMode,
|
|
1185
1187
|
});
|
|
1188
|
+
if (reportOnlyMode && snapshot) {
|
|
1189
|
+
snapshot.itemActions = Object.fromEntries(Object.entries(snapshot.itemActions || {}).map(([id, action]) => [id, action === 'mask' ? 'warn' : action]));
|
|
1190
|
+
snapshot.customBlocks = snapshot.customBlocks.map(rule => rule.action === 'mask' ? { ...rule, action: 'warn' } : rule);
|
|
1191
|
+
}
|
|
1186
1192
|
const requestOutcome = (0, deterministicGuard_1.resolveDeterministicOutcome)(opts => (0, deterministicGuard_1.scanDeterministicToolCall)(toolName, toolArgs, opts), (0, localSafetySnapshot_1.snapshotToScanOptions)(snapshot));
|
|
1187
1193
|
for (const warning of requestOutcome.warnings) {
|
|
1188
1194
|
this.spoolLocalFinding(warning, toolName, operation, 'warn');
|
|
@@ -1238,7 +1244,7 @@ class McpGatewayServer {
|
|
|
1238
1244
|
localBlock = undefined;
|
|
1239
1245
|
}
|
|
1240
1246
|
else {
|
|
1241
|
-
(0, devConfirm_1.
|
|
1247
|
+
(0, devConfirm_1.rejectDeveloperConfirmation)('mcp_gateway', toolName, digest);
|
|
1242
1248
|
const how = answer.answer === 'timeout' ? 'developer confirmation not given (prompt timed out)' : 'developer declined in MCP client prompt';
|
|
1243
1249
|
(0, telemetry_1.spoolEvent)({
|
|
1244
1250
|
decision: 'block', toolName, operation: rememberScope.operation,
|
|
@@ -1311,8 +1317,8 @@ class McpGatewayServer {
|
|
|
1311
1317
|
// MONITOR CONTRACT (latency half): a monitor/no-signal machine makes
|
|
1312
1318
|
// ZERO synchronous network calls. The verdict is always allow; the
|
|
1313
1319
|
// "would block" signal comes from the locally cached policy bundle,
|
|
1314
|
-
// and the console report
|
|
1315
|
-
//
|
|
1320
|
+
// and the console report uses the audit spool. checkToolCall is an
|
|
1321
|
+
// enforcing endpoint: even an unawaited request can open approvals.
|
|
1316
1322
|
const inferred = (0, actionPolicyEngine_1.inferToolContext)(toolName, toolArgs);
|
|
1317
1323
|
const localOperation = operation || inferred.operation;
|
|
1318
1324
|
if (cachedPolicies && cachedPolicies.length > 0) {
|
|
@@ -1333,22 +1339,8 @@ class McpGatewayServer {
|
|
|
1333
1339
|
process.stderr.write(`FullCourtDefense (monitor) would ${wouldWhat} ${toolName}: ${local.reason || local.verdict}\n`);
|
|
1334
1340
|
}
|
|
1335
1341
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
// spool straight to disk; the flush child delivers it when the
|
|
1339
|
-
// network returns. Zero network churn per call during an outage.
|
|
1340
|
-
if ((0, policyGateHealth_1.gateRecentlyDown)()) {
|
|
1341
|
-
(0, telemetry_1.spoolEvent)({ decision: 'allow', toolName, operation: localOperation, reason: 'monitor: action recorded locally (gate recently unreachable)', offlineEnforced: true });
|
|
1342
|
-
(0, telemetry_1.triggerFlush)(false);
|
|
1343
|
-
}
|
|
1344
|
-
else {
|
|
1345
|
-
void this.api.checkToolCall({ toolName, operation, toolArgs }, 1_500)
|
|
1346
|
-
.then(() => (0, policyGateHealth_1.recordGateSuccess)())
|
|
1347
|
-
.catch(() => {
|
|
1348
|
-
(0, telemetry_1.spoolEvent)({ decision: 'allow', toolName, operation: localOperation, reason: 'monitor: action recorded locally (console unreachable)', offlineEnforced: true });
|
|
1349
|
-
(0, telemetry_1.triggerFlush)(false);
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1342
|
+
(0, telemetry_1.spoolEvent)({ decision: 'allow', toolName, operation: localOperation, reason: 'monitor: action allowed and recorded without requesting approval' });
|
|
1343
|
+
(0, telemetry_1.triggerFlush)(false);
|
|
1352
1344
|
preflight = { allowed: true, decision: 'allow', operation: localOperation };
|
|
1353
1345
|
}
|
|
1354
1346
|
else if (cachedPolicies && cachedPolicies.length > 0) {
|
|
@@ -1388,6 +1380,8 @@ class McpGatewayServer {
|
|
|
1388
1380
|
// "Warn only": the call runs, the fleet records Warned (not a silent
|
|
1389
1381
|
// allow). Final verdict — no gate round trip; the audit rides the spool.
|
|
1390
1382
|
const reason = local.reason || `${toolName}: flagged by org Action Policy`;
|
|
1383
|
+
policyWarning = reason;
|
|
1384
|
+
process.stderr.write(`FullCourtDefense warning: ${reason}\n`);
|
|
1391
1385
|
(0, telemetry_1.spoolEvent)({ decision: 'warn', toolName, operation: localOperation, reason: `policy warn (local-first): ${reason}`, ruleId: local.policyName ? `action-policy:${local.policyName}` : undefined, offlineEnforced: true });
|
|
1392
1386
|
(0, telemetry_1.triggerFlush)(false);
|
|
1393
1387
|
preflight = { allowed: true, decision: 'allow', operation: localOperation };
|
|
@@ -1470,9 +1464,9 @@ class McpGatewayServer {
|
|
|
1470
1464
|
process.stderr.write(`FullCourtDefense offline: policy service unreachable (${detail}); ${toolName} checked against locally cached org policies → ${blocking ? `${local.verdict} (report-only)` : 'allow'}.\n`);
|
|
1471
1465
|
preflight = { allowed: true, decision: 'allow', operation: localOperation };
|
|
1472
1466
|
}
|
|
1473
|
-
else if (this.gatewayConfig.failClosed &&
|
|
1467
|
+
else if (this.gatewayConfig.failClosed && !reportOnlyMode) {
|
|
1474
1468
|
// Fail-closed only bites when the machine is actually in block mode
|
|
1475
|
-
//
|
|
1469
|
+
// An explicit --fail-closed flag never overrides Shield Monitor.
|
|
1476
1470
|
// A monitor machine must never hard-block — not even during an
|
|
1477
1471
|
// outage with a bundle-supplied failClosed=true left over.
|
|
1478
1472
|
(0, telemetry_1.spoolEvent)({ decision: 'block', toolName, operation, reason: `fail-closed: ${detail}`, offlineEnforced: true });
|
|
@@ -1575,13 +1569,13 @@ class McpGatewayServer {
|
|
|
1575
1569
|
}
|
|
1576
1570
|
}
|
|
1577
1571
|
if (responseOutcome.maskFindings.length > 0) {
|
|
1578
|
-
// mask-action rules
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1572
|
+
// Enforcing mask-action rules redact matched secrets in place.
|
|
1573
|
+
// Monitor snapshots downgrade mask to warn before evaluation.
|
|
1574
|
+
// If the result shape cannot be rewritten (secret hidden
|
|
1581
1575
|
// in a non-text payload), fail strict and block — a mask rule must
|
|
1582
1576
|
// never leak what it was set to hide — except under monitor, where
|
|
1583
1577
|
// the contract is observe-only: record the would-block and pass the
|
|
1584
|
-
// response through
|
|
1578
|
+
// original response through.
|
|
1585
1579
|
const { masked, fullyMasked } = maskToolResult(rawResult, responseOutcome.maskFindings);
|
|
1586
1580
|
if (!fullyMasked) {
|
|
1587
1581
|
const finding = responseOutcome.maskFindings[0];
|
|
@@ -1642,10 +1636,11 @@ class McpGatewayServer {
|
|
|
1642
1636
|
let finalResult = rawResult;
|
|
1643
1637
|
if (effectiveScanResponse) {
|
|
1644
1638
|
try {
|
|
1645
|
-
|
|
1639
|
+
const scannedResult = await this.api.scanToolResponse({ toolName, operation, toolArgs, result: rawResult });
|
|
1640
|
+
finalResult = reportOnlyMode ? rawResult : scannedResult;
|
|
1646
1641
|
}
|
|
1647
1642
|
catch (scanErr) {
|
|
1648
|
-
if (isBlockError(scanErr))
|
|
1643
|
+
if (isBlockError(scanErr) && !reportOnlyMode)
|
|
1649
1644
|
throw scanErr; // Shield verdict — a real block
|
|
1650
1645
|
// Scan API/infra failure. On an ENFORCING machine, returning the raw
|
|
1651
1646
|
// response would silently skip the org's exfil scan — that is a
|
|
@@ -1656,7 +1651,7 @@ class McpGatewayServer {
|
|
|
1656
1651
|
(0, telemetry_1.triggerFlush)(true);
|
|
1657
1652
|
throw blockError(`FullCourtDefense could not scan the ${toolName} response (${detail}) and this machine enforces response scanning — response withheld. Retry once the scan service is reachable.`);
|
|
1658
1653
|
}
|
|
1659
|
-
(0, telemetry_1.spoolEvent)({ decision: 'warn', toolName, operation, reason: `[monitor] response scan unavailable: ${detail.slice(0, 200)}`, offlineEnforced: true });
|
|
1654
|
+
(0, telemetry_1.spoolEvent)({ decision: 'warn', toolName, operation, reason: `[monitor] ${isBlockError(scanErr) ? 'response would be blocked' : 'response scan unavailable'}: ${detail.slice(0, 200)}`, offlineEnforced: true });
|
|
1660
1655
|
(0, telemetry_1.triggerFlush)(false);
|
|
1661
1656
|
process.stderr.write(`FullCourtDefense (monitor): response scan unavailable for ${toolName} (${detail}) — response passed through.\n`);
|
|
1662
1657
|
finalResult = rawResult;
|
|
@@ -1678,6 +1673,10 @@ class McpGatewayServer {
|
|
|
1678
1673
|
(0, telemetry_1.triggerFlush)(false);
|
|
1679
1674
|
});
|
|
1680
1675
|
}
|
|
1676
|
+
if (policyWarning && finalResult && typeof finalResult === 'object') {
|
|
1677
|
+
const result = finalResult;
|
|
1678
|
+
finalResult = { ...result, content: [...(Array.isArray(result.content) ? result.content : []), { type: 'text', text: `FullCourtDefense warning: ${policyWarning}` }] };
|
|
1679
|
+
}
|
|
1681
1680
|
return finalResult;
|
|
1682
1681
|
}
|
|
1683
1682
|
catch (error) {
|
|
@@ -1713,6 +1712,14 @@ class McpGatewayServer {
|
|
|
1713
1712
|
content: [{ type: 'text', text: 'This machine is suspended by your security team — all AI agent actions are blocked. Contact your admin to resume it.' }],
|
|
1714
1713
|
};
|
|
1715
1714
|
}
|
|
1715
|
+
if (!isBlockError(error) && !downstreamAttempted && !reportOnlyMode && this.gatewayConfig.failClosed) {
|
|
1716
|
+
try {
|
|
1717
|
+
(0, telemetry_1.spoolEvent)({ decision: 'block', toolName, operation, reason: `fail-closed: gateway evaluation failed: ${message.slice(0, 200)}`, offlineEnforced: true });
|
|
1718
|
+
(0, telemetry_1.triggerFlush)(true);
|
|
1719
|
+
}
|
|
1720
|
+
catch { /* preserve the configured decision even when telemetry fails */ }
|
|
1721
|
+
return { isError: true, blocked: true, content: [{ type: 'text', text: 'FullCourtDefense could not verify this action. Your administrator enabled fail-closed. Retry after policy checking recovers.' }] };
|
|
1722
|
+
}
|
|
1716
1723
|
if (!isBlockError(error) && downstreamRan) {
|
|
1717
1724
|
// Post-call scan bug: the tool already succeeded — return its result.
|
|
1718
1725
|
try {
|
|
@@ -1807,7 +1814,7 @@ class McpGatewayServer {
|
|
|
1807
1814
|
const sessionId = runtimeSessionId() || this.gatewaySessionId;
|
|
1808
1815
|
const policyLabel = verdict.policyName ? ` (${verdict.policyName})` : '';
|
|
1809
1816
|
const ruleId = verdict.policyName ? `action-policy:${verdict.policyName}` : undefined;
|
|
1810
|
-
const remembered = (0, devConfirm_1.isConfirmationRemembered)(rememberScope, sessionId);
|
|
1817
|
+
const remembered = verdict.approval?.remember === 'none' ? undefined : (0, devConfirm_1.isConfirmationRemembered)(rememberScope, sessionId);
|
|
1811
1818
|
if (remembered) {
|
|
1812
1819
|
(0, telemetry_1.spoolEvent)({
|
|
1813
1820
|
decision: 'allow', toolName, operation,
|
|
@@ -1837,11 +1844,11 @@ class McpGatewayServer {
|
|
|
1837
1844
|
const answer = await this.elicitDeveloperConfirmation({
|
|
1838
1845
|
toolName,
|
|
1839
1846
|
reason: `${verdict.reason || `${toolName} needs your confirmation`}.${ceiling}`,
|
|
1847
|
+
allowRemember: false,
|
|
1840
1848
|
ruleLabel: verdict.policyName ? `Action Policy "${verdict.policyName}"${verdict.matchedRule ? ` — ${verdict.matchedRule}` : ''}` : 'Action Policy',
|
|
1841
1849
|
});
|
|
1842
1850
|
if (answer.answer === 'accept') {
|
|
1843
|
-
|
|
1844
|
-
(0, devConfirm_1.raiseDeveloperConfirmation)({ ...marker, remember: 'session' });
|
|
1851
|
+
// Policy memory limits are authoritative; a client cannot upgrade 'none'.
|
|
1845
1852
|
(0, devConfirm_1.settleDeveloperConfirmation)('mcp_gateway', toolName, digest);
|
|
1846
1853
|
(0, telemetry_1.spoolEvent)({
|
|
1847
1854
|
decision: 'allow', toolName, operation,
|
|
@@ -1851,7 +1858,7 @@ class McpGatewayServer {
|
|
|
1851
1858
|
(0, telemetry_1.triggerFlush)(false);
|
|
1852
1859
|
return true;
|
|
1853
1860
|
}
|
|
1854
|
-
(0, devConfirm_1.
|
|
1861
|
+
(0, devConfirm_1.rejectDeveloperConfirmation)('mcp_gateway', toolName, digest);
|
|
1855
1862
|
const how = answer.answer === 'timeout' ? 'developer confirmation not given (prompt timed out)' : 'developer declined in MCP client prompt';
|
|
1856
1863
|
(0, telemetry_1.spoolEvent)({
|
|
1857
1864
|
decision: 'block', toolName, operation,
|
|
@@ -1875,12 +1882,12 @@ class McpGatewayServer {
|
|
|
1875
1882
|
description: 'Yes runs the tool call once. No (or closing this dialog) does not run it.',
|
|
1876
1883
|
default: false,
|
|
1877
1884
|
},
|
|
1878
|
-
remember: {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1885
|
+
...(input.allowRemember === false ? {} : { remember: {
|
|
1886
|
+
type: 'boolean',
|
|
1887
|
+
title: 'Remember for this session',
|
|
1888
|
+
description: 'Do not ask again for this tool and rule until this MCP session ends (max 12 hours).',
|
|
1889
|
+
default: false,
|
|
1890
|
+
} }),
|
|
1884
1891
|
},
|
|
1885
1892
|
required: ['confirm'],
|
|
1886
1893
|
},
|
|
@@ -1891,7 +1898,7 @@ class McpGatewayServer {
|
|
|
1891
1898
|
const result = (response.result && typeof response.result === 'object') ? response.result : undefined;
|
|
1892
1899
|
const content = (result?.content && typeof result.content === 'object') ? result.content : undefined;
|
|
1893
1900
|
if (result?.action === 'accept' && content?.confirm === true) {
|
|
1894
|
-
return { answer: 'accept', remember: content.remember === true, latencyMs };
|
|
1901
|
+
return { answer: 'accept', remember: input.allowRemember !== false && content.remember === true, latencyMs };
|
|
1895
1902
|
}
|
|
1896
1903
|
return { answer: 'decline', remember: false, latencyMs };
|
|
1897
1904
|
}
|
|
@@ -173,10 +173,10 @@ function buildGuardJs(nodePath, cliEntry) {
|
|
|
173
173
|
`// exec), $( ) substitution, or real backtick command substitution (>=2 chars; a 1-char pair is a PS escape).`,
|
|
174
174
|
`function execView(line){if(/\\b(?:iex|invoke-expression|invoke-command)\\b|\\beval\\b|\\bxargs\\b|\\bbase64\\b[^\\n]*(?:-d\\b|--decode\\b)|frombase64string|-encodedcommand\\b|\\s-enc\\s|\\|\\s*&?\\s*(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\\.exe)?|cmd(?:\\.exe)?|node|python[0-9.]*|perl|ruby)\\b|\\b(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\\.exe)?|cmd(?:\\.exe)?|node|python[0-9.]*|perl|ruby)\\s+(?:-c|\\/c|-e|-command|-scriptblock)\\b|\\b(?:ssh|wsl(?:\\.exe)?|chroot)\\b|\\b(?:docker|kubectl|podman|nerdctl)(?:\\.exe)?\\s+(?:exec|run)\\b/i.test(line))return line;const K=/\\$\\(|\`[^\`]{2,}\`/;const C=/(?:(?:^|[;&|(]|\\s)(?:-c|\\/c|-e|-command|-scriptblock|-filter)[= ]\\s*|(?:^|[;&|(]\\s*)(?:\\S*[\\\\\\/])?(?:sudo\\s+|doas\\s+)?(?:ssh|wsl(?:\\.exe)?|chroot|su|screen|tmux)(?:\\s[^;&|"']*)?\\s|(?:\\S*[\\\\\\/])?(?:docker|kubectl|podman|nerdctl)(?:\\.exe)?\\s+(?:exec|run)\\b[^;&|"']*\\s)$/i;let o=line;o=o.replace(/@'[\\s\\S]*?'@/g,' fcd_inert_data ');o=o.replace(/@"[\\s\\S]*?"@/g,m=>m.includes('$(')?m:' fcd_inert_data ');o=o.replace(/<<-?\\s*'(\\w+)'[\\s\\S]*?(?:\\n\\1\\b|$)/g,' fcd_inert_data ');o=o.replace(/<<-?\\s*"?(\\w+)"?[\\s\\S]*?(?:\\n\\1\\b|$)/g,m=>K.test(m)?m:' fcd_inert_data ');const s=o;o=s.replace(/"((?:[^"\\\\]|\\\\.)*)"|'((?:[^'\\\\]|\\\\.)*)'/g,(w,dq,sq,i)=>{const b=dq!==undefined?dq:(sq||'');if(!/\\s/.test(b))return w;if(C.test(s.slice(Math.max(0,i-80),i)))return w;if(dq!==undefined&&K.test(b))return w;return ' fcd_inert_data ';});return o;}`,
|
|
175
175
|
`// Precedence on one line: block outranks ask, ask outranks warn.`,
|
|
176
|
-
`function matchLine(line,rules){const n=line.replace(/\\s+/g,' ').trim();const x=execView(line);const xn=x.replace(/\\s+/g,' ').trim();let warnHit=null
|
|
176
|
+
`function matchLine(line,rules){const n=line.replace(/\\s+/g,' ').trim();const x=execView(line);const xn=x.replace(/\\s+/g,' ').trim();let warnHit=null;const asks=[];for(const r of rules){try{const t=r.source==='custom'?line:x;const tn=r.source==='custom'?n:xn;if(r.re.test(tn)||r.re.test(t)){if(r.action==='block')return r;if(r.action==='ask'){asks.push(r);}else if(!warnHit)warnHit=r;}}catch{}}if(asks.length===1)return asks[0];if(asks.length>1){asks.sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0);return{...asks[0],id:'asks:'+asks.map(r=>encodeURIComponent(r.id)).join('+'),reason:asks.map(r=>r.reason).join('; ')}}return warnHit;}`,
|
|
177
177
|
`function triggerFlush(){try{if(fs.existsSync(NODE_PATH)&&fs.existsSync(CLI_ENTRY)){const c=spawn(NODE_PATH,[CLI_ENTRY,'flush-spool','--heartbeat','true'],{detached:true,stdio:'ignore'});c.unref();}}catch{}}`,
|
|
178
178
|
`// developer: '' | 'confirmed' | 'remembered' | 'declined' | 'timeout' | 'ide' | 'agent-terminal' — the "ask" outcome, reported with the same facts as an IDE prompt.`,
|
|
179
|
-
`function spoolEvent(rule,line,decision,developer){try{const ev=line.trim();const evidence=ev.length>180?ev.slice(0,180)+'...':ev;let reason='Shell guard: '+rule.reason;if(developer==='confirmed')reason='developer confirmed in terminal prompt — '+reason;else if(developer==='remembered')reason='developer confirmed earlier in this shell (session memory) — '+reason;else if(developer==='declined')reason='developer declined in terminal prompt — '+reason;else if(developer==='timeout')reason='developer confirmation not given (terminal prompt timed out) — '+reason;else if(developer==='ide')reason='
|
|
179
|
+
`function spoolEvent(rule,line,decision,developer){try{const ev=line.trim();const evidence=ev.length>180?ev.slice(0,180)+'...':ev;let reason='Shell guard: '+rule.reason;if(developer==='confirmed')reason='developer confirmed in terminal prompt — '+reason;else if(developer==='remembered')reason='developer confirmed earlier in this shell (session memory) — '+reason;else if(developer==='declined')reason='developer declined in terminal prompt — '+reason;else if(developer==='timeout')reason='developer confirmation not given (terminal prompt timed out) — '+reason;else if(developer==='ide')reason='delegated to the IDE permission gate (confirmation recorded by post-hook) — '+reason;else if(developer==='agent-terminal')reason='ask cannot be answered in an agent-driven terminal (no IDE confirmation) — '+reason;const event={eventId:crypto.randomUUID(),type:'verdict',decision,toolName:'shell_terminal',operation:'shell_command',reason,ruleId:rule.id,category:rule.category,severity:rule.severity,source:rule.source,evidence,occurredAt:new Date().toISOString()};if(developer){event.approvalScope='developer';if(developer==='confirmed'||developer==='remembered')event.approvedBy='developer';}fs.appendFileSync(SPOOL_PATH,JSON.stringify(event)+'\\n',{encoding:'utf8',mode:0o600});triggerFlush();}catch{}}`,
|
|
180
180
|
(0, agentTerminal_1.buildAgentTerminalJs)(),
|
|
181
181
|
`let argv=process.argv.slice(2);`,
|
|
182
182
|
`// --record <confirmed|remembered|declined|timeout> <ruleId> -- <line>: the shell answered an ask prompt; report it and exit.`,
|
|
@@ -54,6 +54,9 @@ exports.installShellGuardCommand = installShellGuardCommand;
|
|
|
54
54
|
exports.uninstallShellGuardCommand = uninstallShellGuardCommand;
|
|
55
55
|
const child_process_1 = require("child_process");
|
|
56
56
|
const fs = __importStar(require("fs"));
|
|
57
|
+
const crypto = __importStar(require("crypto"));
|
|
58
|
+
const config_1 = require("../config");
|
|
59
|
+
const localSafetySnapshot_1 = require("../localSafetySnapshot");
|
|
57
60
|
const os = __importStar(require("os"));
|
|
58
61
|
const path = __importStar(require("path"));
|
|
59
62
|
const deterministicGuard_1 = require("./deterministicGuard");
|
|
@@ -379,16 +382,48 @@ function builtinRuleCommandNames() {
|
|
|
379
382
|
function escapeDotNetRegex(literal) {
|
|
380
383
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
381
384
|
}
|
|
382
|
-
/**
|
|
385
|
+
/** Select the enrolled Shield and its exact identity snapshot, never merge old caches. */
|
|
386
|
+
function shellEnrollment() {
|
|
387
|
+
const config = (0, config_1.loadMachineConfig)();
|
|
388
|
+
return {
|
|
389
|
+
shieldId: config.shieldId || process.env.FCD_SHIELD_ID || process.env.FULLCOURTDEFENSE_SHIELD_ID,
|
|
390
|
+
apiUrl: (config.apiUrl || process.env.FULLCOURTDEFENSE_API_URL || process.env.FCD_API_URL || 'https://api.fullcourtdefense.ai').replace(/\/$/, ''),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function cachedSnapshotFiles() {
|
|
394
|
+
try {
|
|
395
|
+
const enrollment = shellEnrollment();
|
|
396
|
+
if (enrollment.shieldId) {
|
|
397
|
+
const identity = (0, localSafetySnapshot_1.hookSnapshotIdentity)();
|
|
398
|
+
const key = crypto.createHash('sha1').update([enrollment.apiUrl, enrollment.shieldId, identity.developerName, identity.machineName].join('|')).digest('hex');
|
|
399
|
+
const name = `local-safety-${key}.json`;
|
|
400
|
+
return fs.existsSync(path.join(SNAPSHOT_CACHE_DIR, name)) ? [name] : [];
|
|
401
|
+
}
|
|
402
|
+
// Legacy standalone guards have no enrollment. Pick one coherent snapshot;
|
|
403
|
+
// never combine a disabled list from one version with actions from another.
|
|
404
|
+
return fs.readdirSync(SNAPSHOT_CACHE_DIR)
|
|
405
|
+
.filter(name => name.startsWith('local-safety-') && name.endsWith('.json'))
|
|
406
|
+
.map(name => { try {
|
|
407
|
+
return { name, at: JSON.parse(fs.readFileSync(path.join(SNAPSHOT_CACHE_DIR, name), 'utf8')).loadedAt || 0 };
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return { name, at: -1 };
|
|
411
|
+
} })
|
|
412
|
+
.filter(entry => entry.at >= 0).sort((a, b) => b.at - a.at || a.name.localeCompare(b.name))
|
|
413
|
+
.slice(0, 1).map(entry => entry.name);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** Cached Shield mode from the active enrollment (offline, never network). */
|
|
383
420
|
function cachedMode() {
|
|
384
421
|
try {
|
|
385
422
|
const parsed = JSON.parse(fs.readFileSync(RUNTIME_CACHE_PATH, 'utf8'));
|
|
423
|
+
const { shieldId } = shellEnrollment();
|
|
386
424
|
const entries = Object.values(parsed).filter(e => e && typeof e === 'object');
|
|
387
425
|
entries.sort((a, b) => (b.fetchedAt || 0) - (a.fetchedAt || 0));
|
|
388
|
-
const mode = entries[0]?.mode;
|
|
389
|
-
// Enforce only on an explicit cached 'block'; missing/unknown = monitor.
|
|
390
|
-
// A machine that has never been told to enforce must not block typed
|
|
391
|
-
// commands (fresh install before the first bundle fetch, wiped cache).
|
|
426
|
+
const mode = shieldId ? parsed[shieldId]?.mode : entries[0]?.mode;
|
|
392
427
|
return mode === 'block' ? 'block' : 'monitor';
|
|
393
428
|
}
|
|
394
429
|
catch {
|
|
@@ -404,7 +439,7 @@ function cachedDisabledItemIds() {
|
|
|
404
439
|
const disabled = new Set();
|
|
405
440
|
let snapshots = 0;
|
|
406
441
|
try {
|
|
407
|
-
for (const file of
|
|
442
|
+
for (const file of cachedSnapshotFiles()) {
|
|
408
443
|
if (!file.startsWith('local-safety-') || !file.endsWith('.json'))
|
|
409
444
|
continue;
|
|
410
445
|
try {
|
|
@@ -443,7 +478,7 @@ function cachedItemActions() {
|
|
|
443
478
|
const ask = new Set();
|
|
444
479
|
const blocked = new Set();
|
|
445
480
|
try {
|
|
446
|
-
for (const file of
|
|
481
|
+
for (const file of cachedSnapshotFiles()) {
|
|
447
482
|
if (!file.startsWith('local-safety-') || !file.endsWith('.json'))
|
|
448
483
|
continue;
|
|
449
484
|
try {
|
|
@@ -481,7 +516,7 @@ function cachedItemActions() {
|
|
|
481
516
|
function cachedCustomRules() {
|
|
482
517
|
const rules = [];
|
|
483
518
|
try {
|
|
484
|
-
for (const file of
|
|
519
|
+
for (const file of cachedSnapshotFiles()) {
|
|
485
520
|
if (!file.startsWith('local-safety-') || !file.endsWith('.json'))
|
|
486
521
|
continue;
|
|
487
522
|
try {
|
|
@@ -524,11 +559,12 @@ function cachedCustomRules() {
|
|
|
524
559
|
function effectiveBuiltinRules() {
|
|
525
560
|
const disabled = cachedDisabledItemIds();
|
|
526
561
|
const itemActions = cachedItemActions();
|
|
562
|
+
const hasSnapshot = cachedSnapshotFiles().length > 0;
|
|
527
563
|
return BUILTIN_RULES
|
|
528
564
|
.filter(rule => !disabled.has(rule.id))
|
|
529
565
|
.map(rule => {
|
|
530
566
|
const action = itemActions.get(rule.id);
|
|
531
|
-
return action ? { ...rule, action } : rule;
|
|
567
|
+
return action ? { ...rule, action } : hasSnapshot ? rule : { ...rule, action: 'warn' };
|
|
532
568
|
});
|
|
533
569
|
}
|
|
534
570
|
/** Write (or rewrite) the ruleset the profile guard loads at shell startup. */
|
|
@@ -707,7 +743,7 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
707
743
|
` try { $execView = Get-FcdExecView -CommandLine $CommandLine } catch { }`,
|
|
708
744
|
` $execNormalized = ($execView -replace '\\s+', ' ').Trim()`,
|
|
709
745
|
` $warnHit = $null`,
|
|
710
|
-
` $
|
|
746
|
+
` $askHits = @()`,
|
|
711
747
|
` foreach ($rule in $global:FcdGuardRules) {`,
|
|
712
748
|
` try {`,
|
|
713
749
|
` # Built-in rules ask "can this text ACT?" — they see the execution`,
|
|
@@ -719,12 +755,19 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
719
755
|
` if ($rule.Regex.IsMatch($targetNorm) -or $rule.Regex.IsMatch($target)) {`,
|
|
720
756
|
` # Precedence on one line: block outranks ask, ask outranks warn.`,
|
|
721
757
|
` if ($rule.Action -eq 'block') { return $rule }`,
|
|
722
|
-
` if ($rule.Action -eq 'ask') {
|
|
758
|
+
` if ($rule.Action -eq 'ask') { $askHits += $rule }`,
|
|
723
759
|
` elseif ($null -eq $warnHit) { $warnHit = $rule }`,
|
|
724
760
|
` }`,
|
|
725
761
|
` } catch { }`,
|
|
726
762
|
` }`,
|
|
727
|
-
` if ($
|
|
763
|
+
` if ($askHits.Count -eq 1) { return $askHits[0] }`,
|
|
764
|
+
` if ($askHits.Count -gt 1) {`,
|
|
765
|
+
` $askHits = @($askHits | Sort-Object -Property Id -CaseSensitive)`,
|
|
766
|
+
` $combined = $askHits[0].PSObject.Copy()`,
|
|
767
|
+
` $combined.Id = 'asks:' + (($askHits | ForEach-Object { [Uri]::EscapeDataString($_.Id) }) -join '+')`,
|
|
768
|
+
` $combined.Reason = ($askHits | ForEach-Object { $_.Reason }) -join '; '`,
|
|
769
|
+
` return $combined`,
|
|
770
|
+
` }`,
|
|
728
771
|
` return $warnHit`,
|
|
729
772
|
`}`,
|
|
730
773
|
``,
|
|
@@ -774,7 +817,7 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
774
817
|
` 'remembered' { $reason = 'developer confirmed earlier in this shell (session memory) — ' + $reason }`,
|
|
775
818
|
` 'declined' { $reason = 'developer declined in terminal prompt — ' + $reason }`,
|
|
776
819
|
` 'timeout' { $reason = 'developer confirmation not given (terminal prompt timed out) — ' + $reason }`,
|
|
777
|
-
` 'ide' { $reason = '
|
|
820
|
+
` 'ide' { $reason = 'delegated to the IDE permission gate (confirmation recorded by post-hook) — ' + $reason }`,
|
|
778
821
|
` 'agent-terminal' { $reason = 'ask cannot be answered in an agent-driven terminal (no IDE confirmation) — ' + $reason }`,
|
|
779
822
|
` }`,
|
|
780
823
|
` $event = [ordered]@{`,
|
|
@@ -793,7 +836,7 @@ function buildGuardPs1(nodePath, cliEntry) {
|
|
|
793
836
|
` }`,
|
|
794
837
|
` if ($Developer -ne '') {`,
|
|
795
838
|
` $event['approvalScope'] = 'developer'`,
|
|
796
|
-
` if ($Developer -eq 'confirmed' -or $Developer -eq 'remembered'
|
|
839
|
+
` if ($Developer -eq 'confirmed' -or $Developer -eq 'remembered') { $event['approvedBy'] = 'developer' }`,
|
|
797
840
|
` }`,
|
|
798
841
|
` $event = [pscustomobject]$event`,
|
|
799
842
|
` # BOM-free UTF8 append — PS5's Add-Content -Encoding utf8 writes a BOM that breaks JSONL parsing.`,
|
|
@@ -53,6 +53,7 @@ const mcpGateway_1 = require("./mcpGateway");
|
|
|
53
53
|
const discoverSchedule_1 = require("./discoverSchedule");
|
|
54
54
|
const installCursorHook_1 = require("./installCursorHook");
|
|
55
55
|
const installClaudeHook_1 = require("./installClaudeHook");
|
|
56
|
+
const installNativeHooks_1 = require("./installNativeHooks");
|
|
56
57
|
const shellGuard_1 = require("./shellGuard");
|
|
57
58
|
const posixShellGuard_1 = require("./posixShellGuard");
|
|
58
59
|
const cmdGuard_1 = require("./cmdGuard");
|
|
@@ -246,6 +247,7 @@ async function runLocalDetach(args, config) {
|
|
|
246
247
|
{ label: 'MCP gateway wraps (restore original server commands)', run: () => (0, mcpGateway_1.unprotectAllCommand)({ ...args, clients: args.clients || 'all' }, config) },
|
|
247
248
|
{ label: 'Cursor runtime hooks', run: () => (0, installCursorHook_1.uninstallCursorHookCommand)({}) },
|
|
248
249
|
{ label: 'Claude Code / VS Code / Copilot CLI hooks', run: () => (0, installClaudeHook_1.uninstallClaudeHookCommand)({}) },
|
|
250
|
+
{ label: 'Codex / Gemini / Windsurf / current Kiro project hooks', run: () => { (0, installNativeHooks_1.installNativeHooks)({ remove: true }); } },
|
|
249
251
|
...(process.platform === 'win32'
|
|
250
252
|
? [
|
|
251
253
|
{ label: 'PowerShell shell guard', run: () => (0, shellGuard_1.uninstallShellGuardCommand)() },
|
package/dist/config.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export declare function dpapiRoundtripProbe(): {
|
|
|
61
61
|
detail: string;
|
|
62
62
|
};
|
|
63
63
|
export declare function getHomeConfigPath(): string;
|
|
64
|
+
/** Machine enrollment only; terminal guards must not inherit a repository's Shield. */
|
|
65
|
+
export declare function loadMachineConfig(): BotGuardConfig;
|
|
64
66
|
export declare function loadConfig(configPath?: string): BotGuardConfig;
|
|
65
67
|
export declare function getDefaultConfigPath(): string;
|
|
66
68
|
export declare function saveShieldConfig(input: {
|
package/dist/config.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.getPowershellHealth = getPowershellHealth;
|
|
|
38
38
|
exports.capturePowershellLanguageMode = capturePowershellLanguageMode;
|
|
39
39
|
exports.dpapiRoundtripProbe = dpapiRoundtripProbe;
|
|
40
40
|
exports.getHomeConfigPath = getHomeConfigPath;
|
|
41
|
+
exports.loadMachineConfig = loadMachineConfig;
|
|
41
42
|
exports.loadConfig = loadConfig;
|
|
42
43
|
exports.getDefaultConfigPath = getDefaultConfigPath;
|
|
43
44
|
exports.saveShieldConfig = saveShieldConfig;
|
|
@@ -295,6 +296,10 @@ function mergeConfig(base, override) {
|
|
|
295
296
|
function getHomeConfigPath() {
|
|
296
297
|
return path.join(os.homedir(), '.fullcourtdefense.yml');
|
|
297
298
|
}
|
|
299
|
+
/** Machine enrollment only; terminal guards must not inherit a repository's Shield. */
|
|
300
|
+
function loadMachineConfig() {
|
|
301
|
+
return readConfigFile(getHomeConfigPath());
|
|
302
|
+
}
|
|
298
303
|
function loadConfig(configPath) {
|
|
299
304
|
const homeConfig = readConfigFile(getHomeConfigPath());
|
|
300
305
|
let projectConfig = {};
|
package/dist/devConfirm.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface PendingConfirmation {
|
|
|
13
13
|
sessionId?: string;
|
|
14
14
|
remember: ConfirmRemember;
|
|
15
15
|
raisedAt: string;
|
|
16
|
+
/** Exact-command handoff to the IDE permission gate; not proof of consent. */
|
|
17
|
+
permissionHandoff?: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface RememberedConfirmation {
|
|
18
20
|
/**
|
|
@@ -65,6 +67,8 @@ export declare function settleDeveloperConfirmation(event: string, toolName: str
|
|
|
65
67
|
marker: PendingConfirmation;
|
|
66
68
|
confirmLatencyMs: number;
|
|
67
69
|
} | undefined;
|
|
70
|
+
/** Reject or cancel a pending request without creating approval memory. */
|
|
71
|
+
export declare function rejectDeveloperConfirmation(event: string, toolName: string, argsDigest: string): void;
|
|
68
72
|
/**
|
|
69
73
|
* Markers older than the TTL never saw a post-execution hook: the developer denied the IDE
|
|
70
74
|
* prompt, dismissed it, or the client ignored `ask`. Removed and returned so the caller can
|