fullcourtdefense-cli 1.21.6 → 1.21.7
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/commands/desktopChatGuard.js +12 -150
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -60,20 +60,23 @@ const discoverPaths_1 = require("./discoverPaths");
|
|
|
60
60
|
* closes that blind spot with the SAME on-device deterministic engine used
|
|
61
61
|
* everywhere else (scanDeterministicPrompt).
|
|
62
62
|
*
|
|
63
|
-
* Reading the composer — MSAA
|
|
63
|
+
* Reading the composer — MSAA, passive best-effort:
|
|
64
64
|
* Claude Desktop is Electron/Chromium. Chromium keeps web-content
|
|
65
65
|
* accessibility OFF until it detects assistive tech, and on current Claude
|
|
66
|
-
* builds
|
|
66
|
+
* builds no passive wake signal turns it on from outside the process
|
|
67
67
|
* (verified against Claude 1.20186: WM_GETOBJECT for OBJID_CLIENT and for the
|
|
68
68
|
* custom screen-reader object id 1, SPI_SETSCREENREADER, a UIA FindAll walk,
|
|
69
69
|
* and an NVDA-style IServiceProvider->IAccessible2 handshake all leave the
|
|
70
|
-
* web area empty).
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
70
|
+
* web area empty). The only reliable switch is launching Claude with
|
|
71
|
+
* --force-renderer-accessibility — but force-restarting the user's app is
|
|
72
|
+
* too disruptive for a fleet product, so we deliberately DON'T. The guard
|
|
73
|
+
* reads via MSAA (oleacc's AccessibleObjectFromWindow on the main
|
|
74
|
+
* Chrome_WidgetWin_1 window, accValue of ROLE_SYSTEM_TEXT / 0x2A nodes) and
|
|
75
|
+
* simply works when accessibility happens to be active (screen-reader users,
|
|
76
|
+
* older builds, or the app launched with the flag) and stays silent when not.
|
|
77
|
+
*
|
|
78
|
+
* The SUPPORTED protection surface for Claude Desktop is its MCP tool calls
|
|
79
|
+
* through the FCD gateway — that is where enforcement happens.
|
|
77
80
|
*
|
|
78
81
|
* Advisory ONLY: no keyboard hook, no input mutation, no keystroke capture. It
|
|
79
82
|
* reads only the composer text of Claude Desktop's own window (scoped to the
|
|
@@ -89,13 +92,6 @@ const discoverPaths_1 = require("./discoverPaths");
|
|
|
89
92
|
*/
|
|
90
93
|
const WATCHER_PS1_PATH = path.join(os.homedir(), '.fullcourtdefense-claude-desktop-watcher.ps1');
|
|
91
94
|
const STATUS_PATH = path.join(os.homedir(), '.fullcourtdefense', 'claude-desktop-guard.json');
|
|
92
|
-
/** Throttle marker for the one-time --force-renderer-accessibility relaunch. */
|
|
93
|
-
const A11Y_RELAUNCH_MARKER = path.join(os.homedir(), '.fullcourtdefense', 'claude-a11y-relaunch.json');
|
|
94
|
-
/** Never relaunch Claude more than once per this window (a paste-time restart
|
|
95
|
-
* is disruptive; once the flag sticks it survives until Claude next updates). */
|
|
96
|
-
const A11Y_RELAUNCH_THROTTLE_MS = 6 * 60 * 60_000;
|
|
97
|
-
/** How often to re-check that Claude is still running with the a11y flag. */
|
|
98
|
-
const A11Y_CHECK_INTERVAL_MS = 5 * 60_000;
|
|
99
95
|
const STDOUT_PREFIX = 'FCD:';
|
|
100
96
|
/** Poll cadence for the composer read (ms). 800ms lost the race against a
|
|
101
97
|
* fast paste+Enter (the composer clears before the next read); 250ms is still
|
|
@@ -279,137 +275,6 @@ function ensureWatcherScript() {
|
|
|
279
275
|
fs.writeFileSync(WATCHER_PS1_PATH, desktopChatWatcherScript(), { encoding: 'utf8' });
|
|
280
276
|
return WATCHER_PS1_PATH;
|
|
281
277
|
}
|
|
282
|
-
function relaunchThrottleOk(currentClaudeCreated) {
|
|
283
|
-
try {
|
|
284
|
-
const marker = JSON.parse(fs.readFileSync(A11Y_RELAUNCH_MARKER, 'utf8'));
|
|
285
|
-
const last = marker.lastAttemptAt || 0;
|
|
286
|
-
const sinceLast = Date.now() - last;
|
|
287
|
-
// Hard floor: never two relaunches within 15 minutes regardless of
|
|
288
|
-
// instance tracking — bounds the worst case if a relaunched Claude drops
|
|
289
|
-
// our flag (e.g. MSIX self-restart) so we can't kill it in a loop.
|
|
290
|
-
if (sinceLast < 15 * 60_000)
|
|
291
|
-
return false;
|
|
292
|
-
// Claude was restarted (update, crash, user quit/reopen) since our last
|
|
293
|
-
// relaunch attempt — the old throttle must not leave the composer blind.
|
|
294
|
-
// Legacy markers without claudeCreated also take this path once, then
|
|
295
|
-
// start tracking the instance.
|
|
296
|
-
if (currentClaudeCreated && marker.claudeCreated !== currentClaudeCreated) {
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
return sinceLast > A11Y_RELAUNCH_THROTTLE_MS;
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
function stampRelaunch(claudeCreated) {
|
|
306
|
-
try {
|
|
307
|
-
fs.mkdirSync(path.dirname(A11Y_RELAUNCH_MARKER), { recursive: true });
|
|
308
|
-
fs.writeFileSync(A11Y_RELAUNCH_MARKER, JSON.stringify({
|
|
309
|
-
lastAttemptAt: Date.now(),
|
|
310
|
-
...(claudeCreated ? { claudeCreated } : {}),
|
|
311
|
-
}), { encoding: 'utf8', mode: 0o600 });
|
|
312
|
-
}
|
|
313
|
-
catch { /* best-effort */ }
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* PowerShell that inspects the running Claude Desktop browser process and, when
|
|
317
|
-
* allowed, relaunches it with --force-renderer-accessibility. Chromium exposes
|
|
318
|
-
* web-content (the chat composer) to MSAA only when that switch is present, and
|
|
319
|
-
* no external/passive signal turns it on for current Claude builds. Emits a
|
|
320
|
-
* single `STATE:<...>` line so the caller can log/throttle honestly.
|
|
321
|
-
*
|
|
322
|
-
* STATE:not-running — nothing to do
|
|
323
|
-
* STATE:has-flag — already accessible, no action
|
|
324
|
-
* STATE:relaunched — was missing the flag; we restarted it with the flag
|
|
325
|
-
* STATE:needs-flag — missing the flag but relaunch not allowed (throttled)
|
|
326
|
-
* STATE:error:<msg>
|
|
327
|
-
*
|
|
328
|
-
* Only ever targets processes named exactly 'claude.exe' from their own install
|
|
329
|
-
* root, and only restarts a process the user already had open.
|
|
330
|
-
*/
|
|
331
|
-
function ensureAccessibilityScript(allowRelaunch) {
|
|
332
|
-
return [
|
|
333
|
-
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
334
|
-
"$procs = Get-CimInstance Win32_Process -Filter \"Name = 'claude.exe'\"",
|
|
335
|
-
'if (-not $procs) { Write-Output \'STATE:not-running\'; exit 0 }',
|
|
336
|
-
// The browser (main) process is the claude.exe with no --type= child switch.
|
|
337
|
-
"$main = $procs | Where-Object { $_.CommandLine -and ($_.CommandLine -notmatch '--type=') } | Select-Object -First 1",
|
|
338
|
-
"if (-not $main) { Write-Output 'STATE:not-running'; exit 0 }",
|
|
339
|
-
"$created = $main.CreationDate",
|
|
340
|
-
"if ($main.CommandLine -match '--force-renderer-accessibility') { Write-Output ('STATE:has-flag:' + $created); exit 0 }",
|
|
341
|
-
allowRelaunch ? '' : "Write-Output ('STATE:needs-flag:' + $created); exit 0",
|
|
342
|
-
"$exe = $main.ExecutablePath",
|
|
343
|
-
"if (-not $exe -or -not (Test-Path $exe)) { Write-Output 'STATE:error:no-exe'; exit 0 }",
|
|
344
|
-
'try {',
|
|
345
|
-
" $procs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
|
|
346
|
-
' Start-Sleep -Milliseconds 1500',
|
|
347
|
-
" Start-Process -FilePath $exe -ArgumentList '--force-renderer-accessibility'",
|
|
348
|
-
" Write-Output ('STATE:relaunched:' + $created)",
|
|
349
|
-
"} catch { Write-Output ('STATE:error:' + $_.Exception.Message) }",
|
|
350
|
-
].filter(Boolean).join('\n');
|
|
351
|
-
}
|
|
352
|
-
function parseAccessibilityState(out) {
|
|
353
|
-
const match = out.match(/STATE:([^:\r\n]+)(?::(.+))?/);
|
|
354
|
-
if (!match)
|
|
355
|
-
return { state: '' };
|
|
356
|
-
return { state: match[1].trim(), claudeCreated: match[2]?.trim() };
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Ensure Claude Desktop is running with renderer accessibility so the composer
|
|
360
|
-
* is readable. Non-blocking best-effort; throttled to at most one relaunch per
|
|
361
|
-
* A11Y_RELAUNCH_THROTTLE_MS for the SAME Claude process instance. A fresh
|
|
362
|
-
* Claude restart (update, MSI, user reopen) bypasses the throttle.
|
|
363
|
-
*/
|
|
364
|
-
function ensureClaudeForceAccessibility(log) {
|
|
365
|
-
if (process.platform !== 'win32')
|
|
366
|
-
return;
|
|
367
|
-
// Probe first without committing to relaunch so we can compare process age
|
|
368
|
-
// against the throttle marker before deciding.
|
|
369
|
-
const probe = (0, child_process_1.spawn)('powershell', [
|
|
370
|
-
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden',
|
|
371
|
-
'-Command', ensureAccessibilityScript(false),
|
|
372
|
-
], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
373
|
-
let probeOut = '';
|
|
374
|
-
probe.stdout?.on('data', (d) => { probeOut += d.toString(); });
|
|
375
|
-
probe.on('error', () => { });
|
|
376
|
-
probe.on('exit', () => {
|
|
377
|
-
const { state, claudeCreated } = parseAccessibilityState(probeOut);
|
|
378
|
-
if (!state || state === 'not-running' || state.startsWith('has-flag'))
|
|
379
|
-
return;
|
|
380
|
-
if (!state.startsWith('needs-flag')) {
|
|
381
|
-
if (state.startsWith('error'))
|
|
382
|
-
log(`Claude Desktop guard: accessibility probe failed (${state}).`);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const allow = relaunchThrottleOk(claudeCreated);
|
|
386
|
-
if (!allow) {
|
|
387
|
-
log('Claude Desktop guard: Claude is running without accessibility; relaunch throttled — composer stays unreadable until the next allowed relaunch.');
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
const child = (0, child_process_1.spawn)('powershell', [
|
|
391
|
-
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden',
|
|
392
|
-
'-Command', ensureAccessibilityScript(true),
|
|
393
|
-
], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
394
|
-
let out = '';
|
|
395
|
-
child.stdout?.on('data', (d) => { out += d.toString(); });
|
|
396
|
-
child.on('error', () => { });
|
|
397
|
-
child.on('exit', () => {
|
|
398
|
-
const result = parseAccessibilityState(out);
|
|
399
|
-
if (result.state === 'relaunched') {
|
|
400
|
-
stampRelaunch(result.claudeCreated || claudeCreated);
|
|
401
|
-
log('Claude Desktop guard: relaunched Claude with --force-renderer-accessibility so the chat composer is readable.');
|
|
402
|
-
}
|
|
403
|
-
else if (result.state.startsWith('error')) {
|
|
404
|
-
// An error can surface AFTER we already force-stopped Claude (Start-Process
|
|
405
|
-
// threw). Stamp the throttle regardless so a half-failed restart can never
|
|
406
|
-
// loop the kill every 5 minutes — wait the full window before retrying.
|
|
407
|
-
stampRelaunch(result.claudeCreated || claudeCreated);
|
|
408
|
-
log(`Claude Desktop guard: could not enable accessibility (${result.state}); backing off before retry.`);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
278
|
/**
|
|
414
279
|
* Start the advisory guard. Returns a handle whose stop() tears down the
|
|
415
280
|
* watcher and timers. Safe no-op (returns undefined) on unsupported platforms.
|
|
@@ -534,10 +399,8 @@ function startDesktopChatGuard(runtime) {
|
|
|
534
399
|
restartDelay = Math.min(RESTART_MAX_MS, restartDelay * 2);
|
|
535
400
|
};
|
|
536
401
|
void refreshSnapshot();
|
|
537
|
-
ensureClaudeForceAccessibility(runtime.log);
|
|
538
402
|
const snapshotTimer = setInterval(() => { void refreshSnapshot(); }, SNAPSHOT_REFRESH_MS);
|
|
539
403
|
const heartbeatTimer = setInterval(heartbeat, 60_000);
|
|
540
|
-
const a11yTimer = setInterval(() => ensureClaudeForceAccessibility(runtime.log), A11Y_CHECK_INTERVAL_MS);
|
|
541
404
|
spawnWatcher();
|
|
542
405
|
return {
|
|
543
406
|
stop() {
|
|
@@ -546,7 +409,6 @@ function startDesktopChatGuard(runtime) {
|
|
|
546
409
|
stopped = true;
|
|
547
410
|
clearInterval(snapshotTimer);
|
|
548
411
|
clearInterval(heartbeatTimer);
|
|
549
|
-
clearInterval(a11yTimer);
|
|
550
412
|
if (restartTimer) {
|
|
551
413
|
clearTimeout(restartTimer);
|
|
552
414
|
restartTimer = undefined;
|
package/dist/version.json
CHANGED