fullcourtdefense-cli 1.22.3 → 1.22.4

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.
@@ -75,6 +75,7 @@ const cmdGuard_1 = require("./cmdGuard");
75
75
  const machineActionVerify_1 = require("../machineActionVerify");
76
76
  const desktopChatGuard_1 = require("./desktopChatGuard");
77
77
  const honeypot_1 = require("../honeypot");
78
+ const windowsAudit_1 = require("./windowsAudit");
78
79
  const COLOR = {
79
80
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
80
81
  red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
@@ -1157,6 +1158,21 @@ async function runDaemon(args, config) {
1157
1158
  }, RESCAN_INTERVAL_MS);
1158
1159
  const bundleTimer = setInterval(() => { void pollBundle(); }, BUNDLE_POLL_MS);
1159
1160
  const heartbeatTimer = setInterval(() => { void heartbeat(); }, HEARTBEAT_INTERVAL_MS);
1161
+ // PowerShell transcript retention (Windows): the Transcription policy FCD
1162
+ // enables writes a file per session forever — prune anything older than the
1163
+ // retention window once a day (plus once shortly after boot, so laptops
1164
+ // that never stay up 24h still get cleaned). Only touches FCD-managed
1165
+ // transcript folders; see pruneTranscripts.
1166
+ const runTranscriptPrune = () => {
1167
+ try {
1168
+ const result = (0, windowsAudit_1.pruneTranscripts)();
1169
+ if (result.pruned > 0)
1170
+ log(`Transcript retention: pruned ${result.pruned} transcript file(s) older than ${(0, windowsAudit_1.transcriptRetentionDays)()} day(s).`);
1171
+ }
1172
+ catch { /* never let housekeeping hurt the daemon */ }
1173
+ };
1174
+ const transcriptPruneBootTimer = setTimeout(runTranscriptPrune, 5 * 60_000);
1175
+ const transcriptPruneTimer = setInterval(runTranscriptPrune, 24 * 60 * 60_000);
1160
1176
  const shutdown = (signal) => {
1161
1177
  if (stopped)
1162
1178
  return;
@@ -1165,6 +1181,8 @@ async function runDaemon(args, config) {
1165
1181
  clearInterval(rescanTimer);
1166
1182
  clearInterval(bundleTimer);
1167
1183
  clearInterval(heartbeatTimer);
1184
+ clearTimeout(transcriptPruneBootTimer);
1185
+ clearInterval(transcriptPruneTimer);
1168
1186
  clearTimeout(discoverCatchUpBootTimer);
1169
1187
  clearInterval(discoverCatchUpTimer);
1170
1188
  if (initialDiscoverTimer)
@@ -84,6 +84,17 @@ const CLIENT_LABELS = {
84
84
  windsurf: 'Windsurf',
85
85
  vscode: 'VS Code',
86
86
  };
87
+ /**
88
+ * Default configured-context template per client. Single source of truth —
89
+ * protect-all wraps configs for EVERY client from one shared GatewayConfig,
90
+ * and deriving the objective from that shared config gave Codex/Claude/etc.
91
+ * entries the Cursor template ("from Cursor" on a Codex event — a customer-
92
+ * reported labeling bug). This is a CONFIGURED template, not observed
93
+ * behavior; the console labels it accordingly.
94
+ */
95
+ function defaultUserObjective(agentClient) {
96
+ return `Developer requested this action from ${CLIENT_LABELS[agentClient] || 'an MCP client'}.`;
97
+ }
87
98
  function parseHeadersFlag(value) {
88
99
  if (!value?.trim())
89
100
  return undefined;
@@ -206,7 +217,8 @@ function resolveGatewayConfig(args, config, defaults = {}) {
206
217
  agentClient,
207
218
  developerName,
208
219
  environment: args.environment || process.env.FCD_ENVIRONMENT || 'developer-workstation',
209
- userObjective: args.userObjective || process.env.FCD_USER_OBJECTIVE || defaults.userObjective || 'Developer requested this action from Cursor.',
220
+ userObjective: args.userObjective || process.env.FCD_USER_OBJECTIVE || defaults.userObjective || defaultUserObjective(agentClient),
221
+ userObjectiveExplicit: !!(args.userObjective || process.env.FCD_USER_OBJECTIVE),
210
222
  authority: args.authority || process.env.FCD_AUTHORITY || 'developer',
211
223
  approvalMode: args.approvalMode === 'block' ? 'block' : 'wait',
212
224
  approvalTimeoutMs: Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 900000,
@@ -606,7 +618,11 @@ class AgentGuardApi {
606
618
  agentName: this.config.agentName,
607
619
  toolName: input.toolName,
608
620
  operation: input.operation,
609
- toolArgs: input.toolArgs,
621
+ // Data minimization: this is a pure audit-record call (the decision was
622
+ // already made by check-tool-call), and the backend persists only
623
+ // argsSummary — so the RAW arguments are redacted/truncated ON THIS
624
+ // MACHINE and never sent here. Raw args still travel on the policy
625
+ // check itself, where the evaluation genuinely needs them.
610
626
  argsSummary: summarizeToolArgs(input.toolArgs, this.config.developerName, this.config.agentClient),
611
627
  environment: this.config.environment,
612
628
  userObjective: this.config.userObjective,
@@ -1352,7 +1368,6 @@ async function installCursorMcpGatewayCommand(args, config) {
1352
1368
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
1353
1369
  agentClient: 'cursor',
1354
1370
  agentNamePrefix: 'cursor',
1355
- userObjective: 'Developer requested this action from Cursor.',
1356
1371
  });
1357
1372
  const projectScope = args.project === 'true';
1358
1373
  const file = cursorMcpPath(projectScope);
@@ -1743,6 +1758,22 @@ function applyPerServerAgentName(gatewayConfig, args, serverName) {
1743
1758
  agentName: perServerAgentName(gatewayConfig.developerName, gatewayConfig.agentClient, serverName),
1744
1759
  };
1745
1760
  }
1761
+ /**
1762
+ * The per-server config protect-all bakes into each wrapped entry. The shared
1763
+ * base config was resolved ONCE (defaulting to Cursor), so every client-
1764
+ * specific field must be re-derived here: the client itself, the per-server
1765
+ * agent name, and the user-objective template — a Codex entry must say
1766
+ * "from Codex", not inherit the base config's Cursor text. An explicit
1767
+ * --user-objective / FCD_USER_OBJECTIVE still wins for all clients.
1768
+ */
1769
+ function perServerGatewayConfig(gatewayConfig, agentClient, serverName) {
1770
+ return {
1771
+ ...gatewayConfig,
1772
+ agentClient,
1773
+ agentName: perServerAgentName(gatewayConfig.developerName, agentClient, serverName),
1774
+ userObjective: gatewayConfig.userObjectiveExplicit ? gatewayConfig.userObjective : defaultUserObjective(agentClient),
1775
+ };
1776
+ }
1746
1777
  function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
1747
1778
  const stats = newWrapStats();
1748
1779
  let json;
@@ -1773,7 +1804,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
1773
1804
  const credentialPairStale = existingArgs.includes('--shield-id') && !existingArgs.includes('--shield-key');
1774
1805
  const downstream = credentialPairStale ? extractFullyUnwrappedDownstream(entry) : null;
1775
1806
  if (!credentialPairStale || downstream) {
1776
- const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
1807
+ const perServer = perServerGatewayConfig(gatewayConfig, agentClient, name);
1777
1808
  entry.command = nodeExe;
1778
1809
  // Path-only upgrades preserve every downstream/config flag exactly.
1779
1810
  // The credential-pair bug requires a safe rebuild from downstream.
@@ -1788,7 +1819,7 @@ function wrapJsonConfigFile(file, gatewayConfig, agentClient, dryRun) {
1788
1819
  stats.skippedManaged.push(name);
1789
1820
  continue;
1790
1821
  }
1791
- const perServer = { ...gatewayConfig, agentClient, agentName: perServerAgentName(gatewayConfig.developerName, agentClient, name) };
1822
+ const perServer = perServerGatewayConfig(gatewayConfig, agentClient, name);
1792
1823
  if (typeof entry.command !== 'string' || !entry.command) {
1793
1824
  // Remote HTTP/SSE MCP: wrap the URL behind a local stdio gateway that
1794
1825
  // enforces policies, then forwards over Streamable HTTP. Original url,
@@ -2003,7 +2034,7 @@ function transformCodexToml(file, mode, gatewayConfig, dryRun) {
2003
2034
  }
2004
2035
  continue;
2005
2036
  }
2006
- const perServer = { ...gatewayConfig, agentClient: 'codex', agentName: perServerAgentName(gatewayConfig.developerName, 'codex', section.name) };
2037
+ const perServer = perServerGatewayConfig(gatewayConfig, 'codex', section.name);
2007
2038
  const wrappedArgs = buildGatewayCommandArgs(perServer, { kind: 'stdio', command: section.command, args: section.args || [] }, INSTALL_GATEWAY_ARGS);
2008
2039
  const indent = lines[section.cmdLine].match(/^\s*/)?.[0] || '';
2009
2040
  lines[section.cmdLine] = `${indent}command = ${JSON.stringify(nodeExe)}`;
@@ -2187,7 +2218,6 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
2187
2218
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
2188
2219
  agentClient: 'claude-code',
2189
2220
  agentNamePrefix: 'claude-code',
2190
- userObjective: 'Developer requested this action from Claude Code.',
2191
2221
  });
2192
2222
  const scope = normalizeClaudeCodeScope(args.scope);
2193
2223
  const nodeExe = process.execPath;
@@ -2251,7 +2281,6 @@ async function installClaudeDesktopMcpGatewayCommand(args, config) {
2251
2281
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
2252
2282
  agentClient: 'claude-desktop',
2253
2283
  agentNamePrefix: 'claude-desktop',
2254
- userObjective: 'Developer requested this action from Claude Desktop.',
2255
2284
  });
2256
2285
  const targets = (0, discoverPaths_1.claudeDesktopInstallTargets)(args.configPath);
2257
2286
  const nodeExe = process.execPath;
@@ -2274,7 +2303,6 @@ async function installCodexMcpGatewayCommand(args, config) {
2274
2303
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
2275
2304
  agentClient: 'codex',
2276
2305
  agentNamePrefix: 'codex',
2277
- userObjective: 'Developer requested this action from Codex.',
2278
2306
  });
2279
2307
  const projectScope = args.project === 'true';
2280
2308
  const file = codexConfigPath(projectScope);
@@ -2293,7 +2321,6 @@ async function installGeminiMcpGatewayCommand(args, config) {
2293
2321
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
2294
2322
  agentClient: 'gemini-cli',
2295
2323
  agentNamePrefix: 'gemini',
2296
- userObjective: 'Developer requested this action from Gemini CLI.',
2297
2324
  });
2298
2325
  const projectScope = args.project === 'true';
2299
2326
  const file = geminiSettingsPath(projectScope);
@@ -2312,7 +2339,6 @@ async function installWindsurfMcpGatewayCommand(args, config) {
2312
2339
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
2313
2340
  agentClient: 'windsurf',
2314
2341
  agentNamePrefix: 'windsurf',
2315
- userObjective: 'Developer requested this action from Windsurf.',
2316
2342
  });
2317
2343
  const file = windsurfConfigPath();
2318
2344
  const nodeExe = process.execPath;
@@ -2330,7 +2356,6 @@ async function installVscodeMcpGatewayCommand(args, config) {
2330
2356
  const baseGatewayConfig = resolveGatewayConfig(args, config, {
2331
2357
  agentClient: 'vscode',
2332
2358
  agentNamePrefix: 'vscode',
2333
- userObjective: 'Developer requested this action from VS Code.',
2334
2359
  });
2335
2360
  const projectScope = args.project !== 'false';
2336
2361
  const nodeExe = process.execPath;
@@ -24,6 +24,22 @@ export declare function getWindowsAuditStatus(): WindowsAuditStatus;
24
24
  * Never throws — telemetry/discovery must not break on a probe failure.
25
25
  */
26
26
  export declare function getShellAuditReport(): ShellAuditReport | undefined;
27
+ export interface AuditPreInstallSnapshot {
28
+ capturedAt: string;
29
+ scriptBlockLogging: {
30
+ present: boolean;
31
+ enableValue?: number;
32
+ };
33
+ transcription: {
34
+ present: boolean;
35
+ enableValue?: number;
36
+ invocationHeader?: number;
37
+ outputDirectory?: string;
38
+ };
39
+ }
40
+ export declare function auditPreInstallSnapshotPath(): string;
41
+ /** Record the audit keys as they were BEFORE FCD changes them (first contact only). Best-effort, never throws. */
42
+ export declare function captureAuditPreInstallSnapshot(): void;
27
43
  export interface EnableWindowsAuditResult {
28
44
  ok: boolean;
29
45
  status: WindowsAuditStatus;
@@ -32,6 +48,13 @@ export interface EnableWindowsAuditResult {
32
48
  /** True when the user saw (and answered) a UAC elevation prompt. */
33
49
  promptShown: boolean;
34
50
  }
51
+ /**
52
+ * Disable ScriptBlock Logging + Transcription (removes the HKLM policy keys)
53
+ * via one UAC prompt. Mirrors enableWindowsAudit's fail-open contract. If the
54
+ * keys are enforced by GPO/Intune they will reappear on the next policy sync
55
+ * — the message says so.
56
+ */
57
+ export declare function disableWindowsAudit(): EnableWindowsAuditResult;
35
58
  /**
36
59
  * Enable ScriptBlock Logging + Transcription via ONE UAC elevation prompt.
37
60
  * Skips silently when already fully enabled. Never throws; on any failure
@@ -39,8 +62,23 @@ export interface EnableWindowsAuditResult {
39
62
  * message — callers must treat that as non-blocking.
40
63
  */
41
64
  export declare function enableWindowsAudit(): EnableWindowsAuditResult;
65
+ export interface TranscriptPruneResult {
66
+ /** Files deleted in this run. */
67
+ pruned: number;
68
+ /** Set when pruning was skipped entirely (not enabled / foreign dir / non-Windows). */
69
+ skipped?: string;
70
+ }
71
+ /** Retention window in days — env override for fleets that want tighter/looser. */
72
+ export declare function transcriptRetentionDays(): number;
73
+ /**
74
+ * Delete transcript files older than the retention window. Never throws;
75
+ * locked/in-use files (an open PowerShell session's live transcript) are
76
+ * skipped and picked up on a later run. Empty date folders are removed.
77
+ */
78
+ export declare function pruneTranscripts(maxAgeDays?: number, dirOverride?: string): TranscriptPruneResult;
42
79
  export interface WindowsAuditArgs {
43
80
  enable?: string;
81
+ disable?: string;
44
82
  }
45
83
  /**
46
84
  * `fullcourtdefense windows-audit` — show (default) or enable (--enable)
@@ -35,7 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getWindowsAuditStatus = getWindowsAuditStatus;
37
37
  exports.getShellAuditReport = getShellAuditReport;
38
+ exports.auditPreInstallSnapshotPath = auditPreInstallSnapshotPath;
39
+ exports.captureAuditPreInstallSnapshot = captureAuditPreInstallSnapshot;
40
+ exports.disableWindowsAudit = disableWindowsAudit;
38
41
  exports.enableWindowsAudit = enableWindowsAudit;
42
+ exports.transcriptRetentionDays = transcriptRetentionDays;
43
+ exports.pruneTranscripts = pruneTranscripts;
39
44
  exports.windowsAuditCommand = windowsAuditCommand;
40
45
  const child_process_1 = require("child_process");
41
46
  const fs = __importStar(require("fs"));
@@ -132,6 +137,35 @@ function defaultTranscriptDir() {
132
137
  const programData = process.env.ProgramData || 'C:\\ProgramData';
133
138
  return path.join(programData, 'FullCourtDefense', 'Transcripts');
134
139
  }
140
+ function auditPreInstallSnapshotPath() {
141
+ const programData = process.env.ProgramData || 'C:\\ProgramData';
142
+ return path.join(programData, 'FullCourtDefense', 'audit-preinstall.json');
143
+ }
144
+ /** Record the audit keys as they were BEFORE FCD changes them (first contact only). Best-effort, never throws. */
145
+ function captureAuditPreInstallSnapshot() {
146
+ try {
147
+ const file = auditPreInstallSnapshotPath();
148
+ if (fs.existsSync(file))
149
+ return;
150
+ const sblValue = regDword(SBL_KEY, 'EnableScriptBlockLogging');
151
+ const trValue = regDword(TRANSCRIPTION_KEY, 'EnableTranscripting');
152
+ const trHeader = regDword(TRANSCRIPTION_KEY, 'EnableInvocationHeader');
153
+ const trDir = regString(TRANSCRIPTION_KEY, 'OutputDirectory');
154
+ const snapshot = {
155
+ capturedAt: new Date().toISOString(),
156
+ scriptBlockLogging: { present: sblValue !== undefined, enableValue: sblValue },
157
+ transcription: {
158
+ present: trValue !== undefined || trHeader !== undefined || trDir !== undefined,
159
+ enableValue: trValue,
160
+ invocationHeader: trHeader,
161
+ outputDirectory: trDir,
162
+ },
163
+ };
164
+ fs.mkdirSync(path.dirname(file), { recursive: true });
165
+ fs.writeFileSync(file, JSON.stringify(snapshot, null, 2), 'utf8');
166
+ }
167
+ catch { /* best-effort: without a snapshot, uninstall falls back to the conservative heuristic */ }
168
+ }
135
169
  /** The elevated payload: sets both policy keys + creates the transcript dir. */
136
170
  function buildEnableScript(transcriptDir) {
137
171
  return [
@@ -148,6 +182,74 @@ function buildEnableScript(transcriptDir) {
148
182
  `New-ItemProperty -Path $tr -Name OutputDirectory -Value $dir -PropertyType String -Force | Out-Null`,
149
183
  ].join('\r\n') + '\r\n';
150
184
  }
185
+ /** The elevated disable payload: removes both policy keys (audit off machine-wide). */
186
+ function buildDisableScript() {
187
+ return [
188
+ `$ErrorActionPreference = 'SilentlyContinue'`,
189
+ `Remove-Item -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging' -Recurse -Force`,
190
+ `Remove-Item -Path 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\Transcription' -Recurse -Force`,
191
+ `exit 0`,
192
+ ].join('\r\n') + '\r\n';
193
+ }
194
+ /**
195
+ * Disable ScriptBlock Logging + Transcription (removes the HKLM policy keys)
196
+ * via one UAC prompt. Mirrors enableWindowsAudit's fail-open contract. If the
197
+ * keys are enforced by GPO/Intune they will reappear on the next policy sync
198
+ * — the message says so.
199
+ */
200
+ function disableWindowsAudit() {
201
+ const before = getWindowsAuditStatus();
202
+ if (!before.supported) {
203
+ return { ok: false, status: before, promptShown: false, message: 'PowerShell audit logging is a Windows feature — skipped on this OS.' };
204
+ }
205
+ if (!before.scriptBlockLogging && !before.transcription) {
206
+ return { ok: true, status: before, promptShown: false, message: 'PowerShell audit logging is already disabled.' };
207
+ }
208
+ const scriptPath = path.join(os.tmpdir(), `fcd-disable-audit-${process.pid}.ps1`);
209
+ const elevatePath = path.join(os.tmpdir(), `fcd-elevate-disable-audit-${process.pid}.ps1`);
210
+ let promptShown = false;
211
+ try {
212
+ fs.writeFileSync(scriptPath, buildDisableScript(), 'utf8');
213
+ const quotedPayload = `"${scriptPath}"`.replace(/'/g, "''");
214
+ fs.writeFileSync(elevatePath, [
215
+ `$ErrorActionPreference = 'Stop'`,
216
+ `try {`,
217
+ ` $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','${quotedPayload}')`,
218
+ ` exit $p.ExitCode`,
219
+ `} catch {`,
220
+ ` exit 1`,
221
+ `}`,
222
+ ].join('\r\n') + '\r\n', 'utf8');
223
+ promptShown = true;
224
+ (0, child_process_1.spawnSync)('powershell.exe', [
225
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', elevatePath,
226
+ ], { encoding: 'utf8', windowsHide: true, timeout: 120000 });
227
+ const after = getWindowsAuditStatus();
228
+ if (!after.scriptBlockLogging && !after.transcription) {
229
+ return { ok: true, status: after, promptShown, message: 'PowerShell audit logging disabled — both policy keys removed. Existing transcripts were NOT deleted.' };
230
+ }
231
+ return {
232
+ ok: false, status: after, promptShown,
233
+ message: 'PowerShell audit logging is still enabled — the admin prompt was declined, or the keys are enforced by GPO/Intune (they re-apply on policy sync). Ask IT to disable via GPO in that case.',
234
+ };
235
+ }
236
+ catch (error) {
237
+ return {
238
+ ok: false, status: getWindowsAuditStatus(), promptShown,
239
+ message: `PowerShell audit logging could not be disabled (${error instanceof Error ? error.message : String(error)}).`,
240
+ };
241
+ }
242
+ finally {
243
+ try {
244
+ fs.unlinkSync(scriptPath);
245
+ }
246
+ catch { /* best effort */ }
247
+ try {
248
+ fs.unlinkSync(elevatePath);
249
+ }
250
+ catch { /* best effort */ }
251
+ }
252
+ }
151
253
  /**
152
254
  * Enable ScriptBlock Logging + Transcription via ONE UAC elevation prompt.
153
255
  * Skips silently when already fully enabled. Never throws; on any failure
@@ -159,6 +261,9 @@ function enableWindowsAudit() {
159
261
  if (!before.supported) {
160
262
  return { ok: false, status: before, promptShown: false, message: 'PowerShell audit logging is a Windows feature — skipped on this OS.' };
161
263
  }
264
+ // Snapshot BEFORE any change (and before the already-enabled early return:
265
+ // "already enabled by the org" is exactly the state uninstall must restore).
266
+ captureAuditPreInstallSnapshot();
162
267
  if (before.scriptBlockLogging && before.transcription) {
163
268
  return { ok: true, status: before, promptShown: false, message: 'PowerShell audit logging already enabled (ScriptBlock Logging + Transcription).' };
164
269
  }
@@ -219,6 +324,66 @@ function enableWindowsAudit() {
219
324
  catch { /* best effort */ }
220
325
  }
221
326
  }
327
+ // ── Transcript retention ────────────────────────────────────────────────────
328
+ // Transcription writes a file per PowerShell session into date-named
329
+ // subfolders; on a busy developer machine that grows unbounded (a customer
330
+ // audit finding). The daemon prunes files older than the retention window
331
+ // once a day. Deliberately conservative: it only ever touches a directory
332
+ // that FCD itself configured (path contains "FullCourtDefense") — an
333
+ // org-managed transcript share set via GPO is never cleaned by us.
334
+ const DEFAULT_TRANSCRIPT_RETENTION_DAYS = 30;
335
+ /** Retention window in days — env override for fleets that want tighter/looser. */
336
+ function transcriptRetentionDays() {
337
+ const raw = Number(process.env.FCD_TRANSCRIPT_RETENTION_DAYS);
338
+ return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : DEFAULT_TRANSCRIPT_RETENTION_DAYS;
339
+ }
340
+ /**
341
+ * Delete transcript files older than the retention window. Never throws;
342
+ * locked/in-use files (an open PowerShell session's live transcript) are
343
+ * skipped and picked up on a later run. Empty date folders are removed.
344
+ */
345
+ function pruneTranscripts(maxAgeDays = transcriptRetentionDays(), dirOverride) {
346
+ if (process.platform !== 'win32' && !dirOverride)
347
+ return { pruned: 0, skipped: 'non-Windows' };
348
+ const dir = dirOverride ?? getWindowsAuditStatus().transcriptionPath;
349
+ if (!dir)
350
+ return { pruned: 0, skipped: 'transcription not configured' };
351
+ if (!/fullcourtdefense/i.test(dir))
352
+ return { pruned: 0, skipped: `transcript dir not FCD-managed (${dir})` };
353
+ if (!fs.existsSync(dir))
354
+ return { pruned: 0, skipped: 'transcript dir does not exist' };
355
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
356
+ let pruned = 0;
357
+ const pruneDir = (target, depth) => {
358
+ let entries;
359
+ try {
360
+ entries = fs.readdirSync(target, { withFileTypes: true });
361
+ }
362
+ catch {
363
+ return;
364
+ }
365
+ for (const entry of entries) {
366
+ const full = path.join(target, entry.name);
367
+ try {
368
+ if (entry.isDirectory()) {
369
+ if (depth < 2)
370
+ pruneDir(full, depth + 1);
371
+ try {
372
+ fs.rmdirSync(full);
373
+ }
374
+ catch { /* not empty — keep */ }
375
+ }
376
+ else if (fs.statSync(full).mtimeMs < cutoff) {
377
+ fs.unlinkSync(full);
378
+ pruned += 1;
379
+ }
380
+ }
381
+ catch { /* locked (live session transcript) or race — next run */ }
382
+ }
383
+ };
384
+ pruneDir(dir, 0);
385
+ return { pruned };
386
+ }
222
387
  /**
223
388
  * `fullcourtdefense windows-audit` — show (default) or enable (--enable)
224
389
  * Windows PowerShell audit coverage on this machine.
@@ -235,6 +400,13 @@ async function windowsAuditCommand(args) {
235
400
  console.log(result.ok ? `\x1b[32m✓ ${result.message}\x1b[0m` : `\x1b[33m⚠ ${result.message}\x1b[0m`);
236
401
  return;
237
402
  }
403
+ if (args.disable === 'true') {
404
+ console.log('Disabling PowerShell audit logging (removes both HKLM policy keys)…');
405
+ console.log('\x1b[2mWindows will show an admin approval prompt (UAC).\x1b[0m');
406
+ const result = disableWindowsAudit();
407
+ console.log(result.ok ? `\x1b[32m✓ ${result.message}\x1b[0m` : `\x1b[33m⚠ ${result.message}\x1b[0m`);
408
+ return;
409
+ }
238
410
  const status = getWindowsAuditStatus();
239
411
  const mark = (on) => (on ? '\x1b[32m✓ enabled\x1b[0m' : '\x1b[31m✗ disabled\x1b[0m');
240
412
  console.log('\n\x1b[1mWindows PowerShell audit coverage\x1b[0m');
package/dist/index.js CHANGED
@@ -150,7 +150,8 @@ function printHelp() {
150
150
  or --mcp-command needed. Use --hooks false to skip runtime hooks.
151
151
  install Alias for install-all.
152
152
  windows-audit Show Windows PowerShell audit coverage (ScriptBlock Logging +
153
- Transcription). Use --enable to turn it on (one admin prompt).
153
+ Transcription). Use --enable to turn it on, --disable to remove
154
+ both policy keys (one admin prompt either way).
154
155
  install-shell-guard Block dangerous commands typed in a terminal (real-time,
155
156
  offline rule cache): PowerShell on Windows, bash + zsh on macOS/Linux.
156
157
  uninstall-shell-guard removes it; shell-guard-status shows coverage.
@@ -698,7 +699,7 @@ async function main() {
698
699
  break;
699
700
  }
700
701
  case 'windows-audit': {
701
- const args = { enable: flags.enable };
702
+ const args = { enable: flags.enable, disable: flags.disable };
702
703
  await (0, windowsAudit_1.windowsAuditCommand)(args);
703
704
  break;
704
705
  }
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.22.3"
2
+ "version": "1.22.4"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.22.3",
3
+ "version": "1.22.4",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "test:browser-credentials-rule": "npm run build && node scripts/test-browser-credentials-rule.js",
25
25
  "test:taint-ledger": "npm run build && node scripts/test-taint-ledger.js",
26
26
  "test:shell-audit": "npm run build && node scripts/test-shell-audit.js",
27
+ "test:audit-restore": "node scripts/test-audit-restore.js",
27
28
  "test:shell-guard": "npm run build && node scripts/test-shell-guard.js",
28
29
  "test:cmd-guard": "npm run build && node scripts/test-cmd-guard.js",
29
30
  "test:posix-guard": "npm run build && node scripts/test-posix-guard.js",
@@ -32,6 +33,7 @@
32
33
  "test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
33
34
  "test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
34
35
  "test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
36
+ "test:user-objective-labels": "npm run build && node scripts/test-user-objective-labels.js",
35
37
  "test:remote-mcp-gateway": "npm run build && node scripts/test-remote-mcp-gateway.js",
36
38
  "test:e2e-onboarding-personas": "npm run build && node scripts/test-e2e-onboarding-personas.js",
37
39
  "test:onboarding-transaction": "npm run build && node scripts/test-onboarding-transaction.js",