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.
@@ -40,6 +40,7 @@ exports.actionDigest = actionDigest;
40
40
  exports.pendingKey = pendingKey;
41
41
  exports.raiseDeveloperConfirmation = raiseDeveloperConfirmation;
42
42
  exports.settleDeveloperConfirmation = settleDeveloperConfirmation;
43
+ exports.rejectDeveloperConfirmation = rejectDeveloperConfirmation;
43
44
  exports.sweepExpiredConfirmations = sweepExpiredConfirmations;
44
45
  exports.isConfirmationRemembered = isConfirmationRemembered;
45
46
  /**
@@ -106,7 +107,8 @@ function readLedger() {
106
107
  const parsed = JSON.parse(fs.readFileSync(ledgerFile(), 'utf8'));
107
108
  return {
108
109
  pending: Array.isArray(parsed?.pending) ? parsed.pending.filter((p) => p && typeof p === 'object') : [],
109
- remembered: Array.isArray(parsed?.remembered) ? parsed.remembered.filter((r) => r && typeof r === 'object') : [],
110
+ // Older ledgers could remember declines. Require fresh affirmative consent.
111
+ remembered: parsed?.formatVersion === 2 && Array.isArray(parsed?.remembered) ? parsed.remembered.filter((r) => r && typeof r === 'object') : [],
110
112
  };
111
113
  }
112
114
  catch {
@@ -116,7 +118,9 @@ function readLedger() {
116
118
  function writeLedger(ledger) {
117
119
  try {
118
120
  fs.mkdirSync(path.dirname(ledgerFile()), { recursive: true });
119
- fs.writeFileSync(ledgerFile(), JSON.stringify(ledger), { encoding: 'utf8', mode: 0o600 });
121
+ const temp = `${ledgerFile()}.${process.pid}.${crypto.randomUUID()}.tmp`;
122
+ fs.writeFileSync(temp, JSON.stringify({ ...ledger, formatVersion: 2 }), { encoding: 'utf8', mode: 0o600 });
123
+ fs.renameSync(temp, ledgerFile());
120
124
  }
121
125
  catch { /* best-effort — losing a record must never change a verdict */ }
122
126
  }
@@ -185,7 +189,7 @@ function settleDeveloperConfirmation(event, toolName, argsDigest, now = new Date
185
189
  if (!marker)
186
190
  return undefined;
187
191
  ledger.pending = ledger.pending.filter(p => p.key !== key);
188
- if (marker.remember !== 'none') {
192
+ if (marker.remember !== 'none' && Date.parse(marker.raisedAt) <= now.getTime() && now.getTime() - Date.parse(marker.raisedAt) < exports.CONFIRM_PENDING_TTL_MS) {
189
193
  const ttl = marker.remember === '1h' ? REMEMBER_1H_MS : REMEMBER_SESSION_MAX_MS;
190
194
  const remembered = {
191
195
  key: rememberKey(marker),
@@ -206,6 +210,13 @@ function settleDeveloperConfirmation(event, toolName, argsDigest, now = new Date
206
210
  writeLedger(ledger);
207
211
  return { marker, confirmLatencyMs: Math.max(0, now.getTime() - Date.parse(marker.raisedAt)) };
208
212
  }
213
+ /** Reject or cancel a pending request without creating approval memory. */
214
+ function rejectDeveloperConfirmation(event, toolName, argsDigest) {
215
+ const ledger = readLedger();
216
+ const key = pendingKey(event, toolName, argsDigest);
217
+ ledger.pending = ledger.pending.filter(p => p.key !== key);
218
+ writeLedger(ledger);
219
+ }
209
220
  /**
210
221
  * Markers older than the TTL never saw a post-execution hook: the developer denied the IDE
211
222
  * prompt, dismissed it, or the client ignored `ask`. Removed and returned so the caller can
@@ -233,7 +244,7 @@ function isConfirmationRemembered(scope, sessionId, now = new Date()) {
233
244
  const hit = ledger.remembered.find(r => r.key === key);
234
245
  if (!hit)
235
246
  return undefined;
236
- if (Date.parse(hit.expiresAt) <= now.getTime())
247
+ if (!Number.isFinite(Date.parse(hit.expiresAt)) || Date.parse(hit.expiresAt) <= now.getTime())
237
248
  return undefined;
238
249
  if (hit.remember === 'session' && (!sessionId || hit.sessionId !== sessionId))
239
250
  return undefined;
package/dist/hookIo.js CHANGED
@@ -129,6 +129,7 @@ function readStdin() {
129
129
  */
130
130
  function mapHookFlags(flags) {
131
131
  return {
132
+ agentClient: flags['agent-client'],
132
133
  event: flags.event,
133
134
  apiUrl: flags['api-url'],
134
135
  shieldId: flags['shield-id'],
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
44
44
  // touching this file.
45
45
  const config_1 = require("./config");
46
46
  const hook_1 = require("./commands/hook");
47
+ const installNativeHooks_1 = require("./commands/installNativeHooks");
47
48
  const hookIo_1 = require("./hookIo");
48
49
  const fs = __importStar(require("fs"));
49
50
  const path = __importStar(require("path"));
@@ -202,6 +203,9 @@ function printHelp() {
202
203
  uninstall-cursor-hook
203
204
  Removes the FullCourtDefense Cursor hook entries.
204
205
  install-claude-hook
206
+ install-native-hooks --clients codex,gemini_cli,windsurf,kiro
207
+ Native tool hooks (Kiro: current project; Codex: trust via /hooks)
208
+ uninstall-native-hooks
205
209
  Installs Claude-format hooks in ~/.claude/settings.json — ONE file
206
210
  enforces Action Policies + Local Safety rules in Claude Code,
207
211
  VS Code Copilot agent mode, and GitHub Copilot CLI.
@@ -731,6 +735,23 @@ async function main() {
731
735
  await (0, hook_1.hookCommand)(args, config);
732
736
  break;
733
737
  }
738
+ case 'install-native-hooks':
739
+ case 'uninstall-native-hooks': {
740
+ const clients = flags.clients?.split(',');
741
+ if (clients?.some(client => !['codex', 'gemini_cli', 'windsurf', 'kiro'].includes(client)))
742
+ throw new Error('Supported native clients: codex,gemini_cli,windsurf,kiro');
743
+ const files = (0, installNativeHooks_1.installNativeHooks)({ clients, project: flags.project, remove: command === 'uninstall-native-hooks' });
744
+ for (const file of files)
745
+ console.log(file);
746
+ if (command === 'install-native-hooks')
747
+ console.log('Restart the clients to load hooks. In Codex, review and trust the new definitions using /hooks. Kiro hooks apply to the selected project.');
748
+ break;
749
+ }
750
+ case 'install-copilot-hook': {
751
+ const { installCopilotHookCommand } = await Promise.resolve().then(() => __importStar(require('./commands/installClaudeHook')));
752
+ installCopilotHookCommand();
753
+ break;
754
+ }
734
755
  case 'mcp-gateway': {
735
756
  const { mcpGatewayCommand } = await Promise.resolve().then(() => __importStar(require('./commands/mcpGateway')));
736
757
  await mcpGatewayCommand(buildGatewayArgs(), config);
@@ -0,0 +1,25 @@
1
+ import { HookArgs } from './commands/hook';
2
+ import { BotGuardConfig } from './config';
3
+ export type NativeClient = 'codex' | 'gemini_cli' | 'windsurf' | 'kiro';
4
+ type Payload = Record<string, any>;
5
+ export declare const NATIVE_CLIENTS: NativeClient[];
6
+ export declare function patchToolCalls(raw: Payload): Payload[];
7
+ export declare function normalizeNativeInput(client: NativeClient, raw: Payload): Payload;
8
+ export declare function nativeVerdict(client: NativeClient, denied: boolean, reason?: string): {
9
+ stdout: string;
10
+ stderr: string;
11
+ exitCode: number;
12
+ };
13
+ export declare function nativeErrorVerdict(client: NativeClient, reason: string): {
14
+ stdout: string;
15
+ stderr: string;
16
+ exitCode: number;
17
+ };
18
+ /** OS-owned dialog: an agent cannot approve by writing `yes` to hook stdin. */
19
+ export declare function confirmNativeAction(reason: string, timeoutMs?: number): Promise<boolean>;
20
+ export declare function evaluateNativeHook(client: NativeClient, raw: Payload, args: HookArgs, config: BotGuardConfig, confirm?: (reason: string) => Promise<boolean>): Promise<{
21
+ stdout: string;
22
+ stderr: string;
23
+ exitCode: number;
24
+ }>;
25
+ export {};
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NATIVE_CLIENTS = void 0;
4
+ exports.patchToolCalls = patchToolCalls;
5
+ exports.normalizeNativeInput = normalizeNativeInput;
6
+ exports.nativeVerdict = nativeVerdict;
7
+ exports.nativeErrorVerdict = nativeErrorVerdict;
8
+ exports.confirmNativeAction = confirmNativeAction;
9
+ exports.evaluateNativeHook = evaluateNativeHook;
10
+ /** Native hook adapters. Never emit an unsupported `ask` to an IDE. */
11
+ const child_process_1 = require("child_process");
12
+ const hook_1 = require("./commands/hook");
13
+ const config_1 = require("./config");
14
+ const runtimeConfig_1 = require("./runtimeConfig");
15
+ const hookIo_1 = require("./hookIo");
16
+ const devConfirm_1 = require("./devConfirm");
17
+ const telemetry_1 = require("./telemetry");
18
+ exports.NATIVE_CLIENTS = ['codex', 'gemini_cli', 'windsurf', 'kiro'];
19
+ function patchToolCalls(raw) {
20
+ const patch = typeof raw.tool_input === 'string' ? raw.tool_input : String(raw.tool_input?.command || raw.tool_input?.patch || '');
21
+ if (!patch.startsWith('*** Begin Patch') || !patch.trimEnd().endsWith('*** End Patch'))
22
+ throw new Error('Unrecognized apply_patch input');
23
+ const calls = [];
24
+ const blocks = patch.split(/(?=^\*\*\* (?:Add|Update|Delete) File: )/m).slice(1);
25
+ for (const block of blocks) {
26
+ const header = /^\*\*\* (Add|Update|Delete) File: (.+)\r?$/m.exec(block);
27
+ if (!header)
28
+ throw new Error('Malformed patch file header');
29
+ const [, operation, file] = header;
30
+ const moved = /^\*\*\* Move to: (.+)\r?$/m.exec(block)?.[1];
31
+ if (operation === 'Delete' || moved)
32
+ calls.push({ ...raw, tool_name: 'mcp__codex-builtin__delete_file', tool_input: { path: file.trim() } });
33
+ if (operation !== 'Delete')
34
+ calls.push({ ...raw, tool_name: 'Edit', tool_input: { path: (moved || file).trim(), new_string: block.split('\n').filter(line => line.startsWith('+')).map(line => line.slice(1)).join('\n') } });
35
+ }
36
+ if (!calls.length)
37
+ throw new Error('Patch contains no recognized file operations');
38
+ return calls;
39
+ }
40
+ function normalizeNativeInput(client, raw) {
41
+ let name = String(raw.hook_event_name || '');
42
+ let tool = String(raw.tool_name || '');
43
+ let input = raw.tool_input || {};
44
+ if (client === 'windsurf') {
45
+ const event = String(raw.agent_action_name || '');
46
+ const info = raw.tool_info || {};
47
+ name = event.startsWith('post_') ? 'PostToolUse' : 'PreToolUse';
48
+ if (event === 'pre_user_prompt')
49
+ return { ...raw, hook_event_name: 'UserPromptSubmit', prompt: info.user_prompt };
50
+ if (event.endsWith('run_command')) {
51
+ tool = 'Bash';
52
+ input = { command: info.command_line, cwd: info.cwd };
53
+ }
54
+ else if (event.endsWith('read_code')) {
55
+ tool = 'Read';
56
+ input = info;
57
+ }
58
+ else if (event.endsWith('write_code')) {
59
+ tool = 'Edit';
60
+ input = info;
61
+ }
62
+ else if (event.endsWith('mcp_tool_use')) {
63
+ tool = `mcp__${info.mcp_server_name}__${info.mcp_tool_name}`;
64
+ input = info.mcp_tool_arguments;
65
+ }
66
+ else
67
+ throw new Error(`Unsupported Windsurf event: ${event}`);
68
+ }
69
+ else if (client === 'gemini_cli') {
70
+ name = { BeforeTool: 'PreToolUse', AfterTool: 'PostToolUse', BeforeAgent: 'UserPromptSubmit' }[name] || name;
71
+ if (raw.mcp_context?.server_name && raw.mcp_context?.tool_name)
72
+ tool = `mcp__${raw.mcp_context.server_name}__${raw.mcp_context.tool_name}`;
73
+ else if (tool.startsWith('mcp_'))
74
+ tool = `mcp__gemini__${tool}`; // retain full name when the server split is ambiguous
75
+ }
76
+ else if (client === 'kiro') {
77
+ name = { preToolUse: 'PreToolUse', postToolUse: 'PostToolUse', userPromptSubmit: 'UserPromptSubmit' }[name] || name;
78
+ if (tool.startsWith('@') && tool.includes('/'))
79
+ tool = `mcp__${tool.slice(1).replace('/', '__')}`;
80
+ }
81
+ if (!['PreToolUse', 'PostToolUse', 'UserPromptSubmit'].includes(name))
82
+ throw new Error(`Unsupported hook event: ${name}`);
83
+ if (name !== 'UserPromptSubmit' && !tool)
84
+ throw new Error('Missing tool name');
85
+ // Unknown function tools still need Action Policy evaluation, not silent ignore.
86
+ let normalized = { ...raw, hook_event_name: name, tool_name: tool, tool_input: input };
87
+ if (name !== 'UserPromptSubmit' && (0, hook_1.normalizeClaudePayload)(normalized)?.event === 'ignore') {
88
+ normalized = { ...normalized, tool_name: `mcp__${client}-builtin__${tool}` };
89
+ }
90
+ return normalized;
91
+ }
92
+ function nativeVerdict(client, denied, reason = '') {
93
+ if (client === 'codex')
94
+ return { stdout: JSON.stringify(denied ? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } } : {}), stderr: '', exitCode: 0 };
95
+ if (client === 'gemini_cli')
96
+ return { stdout: JSON.stringify(denied ? { decision: 'deny', reason } : {}), stderr: '', exitCode: 0 };
97
+ return { stdout: '', stderr: denied ? reason : '', exitCode: denied ? 2 : 0 };
98
+ }
99
+ function nativeErrorVerdict(client, reason) {
100
+ // Structured blocks survive PowerShell wrappers which collapse exit 2 to 1.
101
+ // The legacy Codex block shape is valid for both tool and prompt hooks.
102
+ if (client === 'codex')
103
+ return { stdout: JSON.stringify({ decision: 'block', reason }), stderr: '', exitCode: 0 };
104
+ return nativeVerdict(client, true, reason);
105
+ }
106
+ /** OS-owned dialog: an agent cannot approve by writing `yes` to hook stdin. */
107
+ async function confirmNativeAction(reason, timeoutMs = 60_000) {
108
+ if (!(0, devConfirm_1.isHumanPresent)())
109
+ return false;
110
+ let executable;
111
+ let args;
112
+ const message = `FullCourtDefense — developer approval\n\n${reason.slice(0, 1800)}\n\nAllow this action once?`;
113
+ if (process.platform === 'win32') {
114
+ executable = 'powershell.exe';
115
+ const encoded = Buffer.from(message, 'utf8').toString('base64');
116
+ const script = `$msg=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); $w=New-Object -ComObject WScript.Shell; $answer=$w.Popup($msg,${Math.ceil(timeoutMs / 1000)},'FullCourtDefense',292); if($answer -eq 6){exit 0}else{exit 1}`;
117
+ args = ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')];
118
+ }
119
+ else if (process.platform === 'darwin') {
120
+ executable = '/usr/bin/osascript';
121
+ args = ['-e', 'on run argv', '-e', 'display dialog (item 1 of argv) buttons {"Deny", "Allow once"} default button "Deny" giving up after 60', '-e', 'if gave up of result or button returned of result is not "Allow once" then error number -128', '-e', 'end run', message];
122
+ }
123
+ else {
124
+ executable = 'zenity';
125
+ args = ['--question', '--default-cancel', '--title=FullCourtDefense', `--text=${message}`, '--ok-label=Allow once', '--cancel-label=Deny', `--timeout=${Math.ceil(timeoutMs / 1000)}`];
126
+ }
127
+ return new Promise(resolve => {
128
+ let finished = false;
129
+ const child = (0, child_process_1.spawn)(executable, args, { stdio: 'ignore', windowsHide: true });
130
+ const cancelChild = () => child.kill();
131
+ process.once('exit', cancelChild);
132
+ const done = (allowed) => { if (!finished) {
133
+ finished = true;
134
+ clearTimeout(timer);
135
+ process.removeListener('exit', cancelChild);
136
+ resolve(allowed);
137
+ } };
138
+ const timer = setTimeout(() => { child.kill(); done(false); }, timeoutMs + 1000);
139
+ child.on('error', () => done(false));
140
+ child.on('exit', code => done(code === 0));
141
+ });
142
+ }
143
+ async function evaluateNativeHook(client, raw, args, config, confirm = confirmNativeAction) {
144
+ if (client === 'codex' && raw.tool_name === 'apply_patch') {
145
+ try {
146
+ // Evaluate every file operation before Codex executes the atomic patch.
147
+ for (const call of patchToolCalls(raw)) {
148
+ const result = await evaluateNativeHook(client, call, args, config, confirm);
149
+ const verdict = JSON.parse(result.stdout || '{}');
150
+ if (result.exitCode !== 0 || verdict.decision === 'block' || verdict.decision === 'deny' || verdict.hookSpecificOutput?.permissionDecision === 'deny')
151
+ return result;
152
+ }
153
+ return nativeVerdict(client, false);
154
+ }
155
+ catch (error) {
156
+ return nativeErrorVerdict(client, `Patch validation failed: ${String(error)}`);
157
+ }
158
+ }
159
+ let payload;
160
+ try {
161
+ payload = normalizeNativeInput(client, raw);
162
+ }
163
+ catch (error) {
164
+ return nativeErrorVerdict(client, `FullCourtDefense could not parse the hook: ${String(error)}`);
165
+ }
166
+ return (0, telemetry_1.markVerdictTiming)({ startedAt: Date.now(), path: 'local', agentClient: client }, async () => {
167
+ const result = await (0, hook_1.evaluateHookRequest)({ ...args, agentClient: client }, JSON.stringify(payload), config);
168
+ let decision;
169
+ try {
170
+ decision = result.stdout ? JSON.parse(result.stdout) : {};
171
+ }
172
+ catch {
173
+ return nativeErrorVerdict(client, 'Invalid FullCourtDefense verdict; action denied.');
174
+ }
175
+ if (!decision || typeof decision !== 'object' || Array.isArray(decision))
176
+ return nativeErrorVerdict(client, 'Invalid FullCourtDefense verdict shape; action denied.');
177
+ const permission = decision.hookSpecificOutput?.permissionDecision || decision.permissionDecision || decision.permission;
178
+ let denied = result.exitCode !== 0 || permission === 'deny' || decision.decision === 'block' || decision.continue === false;
179
+ const reason = decision.hookSpecificOutput?.permissionDecisionReason || decision.permissionDecisionReason || decision.reason || decision.user_message || result.stderr || 'FullCourtDefense policy decision.';
180
+ if (permission === 'ask' && !denied) {
181
+ const mapped = (0, hook_1.normalizeClaudePayload)(payload);
182
+ const call = mapped && mapped.event !== 'ignore' ? (0, hook_1.buildToolCall)(mapped.event, mapped.payload) : null;
183
+ // No remembered/pending handoff remains when an external dialog is used.
184
+ if (call && mapped)
185
+ (0, devConfirm_1.rejectDeveloperConfirmation)(mapped.event, call.toolName, (0, devConfirm_1.actionDigest)(call.toolArgs));
186
+ let allowed = false;
187
+ try {
188
+ allowed = (0, devConfirm_1.isHumanPresent)() && await confirm(`${client}: ${payload.tool_name}\n${reason}`);
189
+ }
190
+ catch { /* deny */ }
191
+ denied = !allowed;
192
+ (0, telemetry_1.spoolEvent)({ decision: allowed ? 'allow' : 'block', toolName: call?.toolName || payload.tool_name,
193
+ reason: allowed ? 'Developer approved once in the FullCourtDefense dialog.' : 'Developer declined, confirmation unavailable, or timed out.',
194
+ approvalScope: 'developer', ...(allowed ? { approvedBy: 'developer' } : {}) });
195
+ }
196
+ // Prompt blocks use the documented exit-code contract, not a PreToolUse object.
197
+ if (payload.hook_event_name === 'UserPromptSubmit' && denied)
198
+ return nativeErrorVerdict(client, reason);
199
+ return nativeVerdict(client, denied, reason);
200
+ });
201
+ }
202
+ async function main() {
203
+ const flags = (0, hookIo_1.parseHookFlags)(process.argv.slice(2));
204
+ const client = flags['agent-client'];
205
+ if (!exports.NATIVE_CLIENTS.includes(client))
206
+ throw new Error('Unsupported native hook client');
207
+ const config = (0, config_1.loadConfig)();
208
+ const shieldId = (0, config_1.resolveCliCredentials)(config, { shieldId: flags['shield-id'] }, { skipDpapi: true }).shieldId;
209
+ // Return before the vendor's 130s timeout: some clients fail open on timeout.
210
+ // Bound the entire hook, including multiple patch files and human dialogs.
211
+ const deadline = setTimeout(() => {
212
+ const deny = (0, runtimeConfig_1.isSuspendedByCache)() || (0, runtimeConfig_1.cachedEnforcementState)(shieldId).mode === 'block';
213
+ const result = deny ? nativeErrorVerdict(client, 'FullCourtDefense evaluation deadline exceeded; action not approved.') : nativeVerdict(client, false);
214
+ if (result.stdout)
215
+ process.stdout.write(result.stdout + '\n');
216
+ if (result.stderr)
217
+ process.stderr.write(result.stderr + '\n');
218
+ process.exit(result.exitCode);
219
+ }, 110_000);
220
+ deadline.unref();
221
+ const raw = JSON.parse((await (0, hookIo_1.readStdin)()).replace(/^\uFEFF/, ''));
222
+ const result = await evaluateNativeHook(client, raw, (0, hookIo_1.mapHookFlags)(flags), config);
223
+ clearTimeout(deadline);
224
+ if (result.stdout)
225
+ process.stdout.write(result.stdout + '\n');
226
+ if (result.stderr)
227
+ process.stderr.write(result.stderr + '\n');
228
+ process.exitCode = result.exitCode;
229
+ }
230
+ if (require.main === module)
231
+ main().catch(error => {
232
+ const client = (0, hookIo_1.parseHookFlags)(process.argv.slice(2))['agent-client'];
233
+ const result = nativeErrorVerdict(client, `FullCourtDefense hook failed: ${String(error)}`);
234
+ if (result.stdout)
235
+ process.stdout.write(result.stdout + '\n');
236
+ if (result.stderr)
237
+ process.stderr.write(result.stderr + '\n');
238
+ process.exitCode = result.exitCode;
239
+ });
@@ -168,6 +168,11 @@ export declare function resetCacheOnEnrollment(): void;
168
168
  * "exception = allow" must not become a kill-switch bypass. Reads only the
169
169
  * local cache file (no network, no config parsing).
170
170
  */
171
+ /** Failure handling reads only the requested Shield, never another enrollment. */
172
+ export declare function cachedEnforcementState(shieldId: string | undefined): {
173
+ mode: ShieldMode;
174
+ failClosed: boolean;
175
+ };
171
176
  export declare function isSuspendedByCache(): boolean;
172
177
  /**
173
178
  * Admin scan-folder choices from the most recent cached bundle (any shield):
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getRuntimeBundle = getRuntimeBundle;
37
37
  exports.validateShieldCredentials = validateShieldCredentials;
38
38
  exports.resetCacheOnEnrollment = resetCacheOnEnrollment;
39
+ exports.cachedEnforcementState = cachedEnforcementState;
39
40
  exports.isSuspendedByCache = isSuspendedByCache;
40
41
  exports.getCachedScanRootOverrides = getCachedScanRootOverrides;
41
42
  exports.getCachedExtraScanRoots = getCachedExtraScanRoots;
@@ -434,6 +435,11 @@ function resetCacheOnEnrollment() {
434
435
  * "exception = allow" must not become a kill-switch bypass. Reads only the
435
436
  * local cache file (no network, no config parsing).
436
437
  */
438
+ /** Failure handling reads only the requested Shield, never another enrollment. */
439
+ function cachedEnforcementState(shieldId) {
440
+ const entry = shieldId ? readCacheFile()[shieldId] : undefined;
441
+ return { mode: entry && isMode(entry.mode) ? entry.mode : 'monitor', failClosed: entry?.failClosed === true };
442
+ }
437
443
  function isSuspendedByCache() {
438
444
  try {
439
445
  const cache = readCacheFile();
@@ -1,4 +1,5 @@
1
1
  export interface SpoolEvent {
2
+ agentClient?: string;
2
3
  eventId: string;
3
4
  type: 'verdict';
4
5
  decision: 'allow' | 'block' | 'approval' | 'warn' | 'mask';
@@ -52,6 +53,7 @@ export type VerdictPath = 'ipc' | 'local' | 'fail_open' | 'gateway';
52
53
  */
53
54
  export declare function evidenceFromToolArgs(toolArgs: Record<string, unknown> | undefined): string | undefined;
54
55
  interface VerdictTiming {
56
+ agentClient?: string;
55
57
  startedAt: number;
56
58
  path: VerdictPath;
57
59
  /** Tool args for this evaluation — stamps `evidence` onto every spooled event. */
@@ -64,6 +66,7 @@ export declare function markVerdictTiming<T>(timing: VerdictTiming, fn: () => T)
64
66
  * Keeps the original start time so latency still covers the full wait.
65
67
  */
66
68
  export declare function setVerdictPath(path: VerdictPath): void;
69
+ export declare function setVerdictClient(client: string): void;
67
70
  /** Attach the current tool call's args so every spool in this evaluation carries the command. */
68
71
  export declare function setVerdictToolArgs(toolArgs: Record<string, unknown>): void;
69
72
  /**
package/dist/telemetry.js CHANGED
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.evidenceFromToolArgs = evidenceFromToolArgs;
37
37
  exports.markVerdictTiming = markVerdictTiming;
38
38
  exports.setVerdictPath = setVerdictPath;
39
+ exports.setVerdictClient = setVerdictClient;
39
40
  exports.setVerdictToolArgs = setVerdictToolArgs;
40
41
  exports.restartVerdictTiming = restartVerdictTiming;
41
42
  exports.spoolEvent = spoolEvent;
@@ -156,6 +157,11 @@ function setVerdictPath(path) {
156
157
  if (store)
157
158
  store.path = path;
158
159
  }
160
+ function setVerdictClient(client) {
161
+ const store = verdictTiming.getStore();
162
+ if (store)
163
+ store.agentClient = client;
164
+ }
159
165
  /** Attach the current tool call's args so every spool in this evaluation carries the command. */
160
166
  function setVerdictToolArgs(toolArgs) {
161
167
  const store = verdictTiming.getStore();
@@ -185,6 +191,7 @@ function spoolEvent(event) {
185
191
  occurredAt: event.occurredAt || new Date().toISOString(),
186
192
  type: 'verdict',
187
193
  decision: event.decision,
194
+ agentClient: event.agentClient || timing?.agentClient,
188
195
  toolName: event.toolName,
189
196
  operation: event.operation,
190
197
  reason: event.reason,
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.32.0"
2
+ "version": "1.34.0"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.32.0",
3
+ "version": "1.34.0",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,6 +13,9 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
+ "test:native-hooks": "npm run build && node scripts/test-native-hook-launch.js && node scripts/test-native-hook-edges.js && node scripts/test-client-telemetry.js && node scripts/test-native-hook-clients.js",
17
+ "test:endpoint-approvals": "npm run build && node scripts/audit-endpoint-mcp-shell.js",
18
+ "test:endpoint-clients": "npm run build && node scripts/test-endpoint-client-matrix.js && node scripts/test-copilot-hook-install.js",
16
19
  "build": "tsc && node scripts/copy-attack-corpus.js",
17
20
  "test:deterministic-guard": "npm run build && node scripts/test-deterministic-guard.js",
18
21
  "test:drive-delete-guard": "npm run build && node scripts/test-drive-delete-guard.js",