From 0798f4e1173407a311d18417c8d78ed956ee4382 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Mon, 31 Aug 2026 09:48:22 +0900 Subject: [PATCH] fix(claude): cache desktop policy status probes --- src/claude/desktop-policy.ts | 89 ++++++++++++++++++- .../management/agent-settings-routes.ts | 8 +- tests/claude-desktop-policy.test.ts | 39 ++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/src/claude/desktop-policy.ts b/src/claude/desktop-policy.ts index 511b86e83c..6c8b31b68e 100644 --- a/src/claude/desktop-policy.ts +++ b/src/claude/desktop-policy.ts @@ -1,5 +1,5 @@ /** Read-only, privacy-safe Windows policy diagnosis for Claude Desktop 3P. */ -import { spawnSync } from "node:child_process"; +import { execFile, spawnSync } from "node:child_process"; import { win32 } from "node:path"; import { resolveTrustedWindowsSystemDirectory } from "../lib/windows-elevation"; import { decodeWindowsTextBytes } from "../lib/windows-text"; @@ -23,6 +23,11 @@ export type ClaudeDesktopPolicyProbeRunner = ( args: readonly string[], ) => ClaudeDesktopPolicyProbeResult; +export type ClaudeDesktopPolicyAsyncProbeRunner = ( + file: string, + args: readonly string[], +) => Promise; + export interface ClaudeDesktopPolicyProbeOptions { readonly platform?: NodeJS.Platform; readonly run?: ClaudeDesktopPolicyProbeRunner; @@ -54,6 +59,23 @@ const defaultPolicyProbeRunner: ClaudeDesktopPolicyProbeRunner = (file, args) => }; }; +const defaultAsyncPolicyProbeRunner: ClaudeDesktopPolicyAsyncProbeRunner = (file, args) => new Promise((resolve) => { + execFile(file, [...args], { + encoding: "buffer", + maxBuffer: 64 * 1024, + timeout: POLICY_PROBE_TIMEOUT_MS, + windowsHide: true, + }, (error, stdout) => { + const errorCode = (error as NodeJS.ErrnoException | null)?.code; + resolve({ + status: error === null ? 0 : typeof errorCode === "number" ? errorCode : null, + stdout: stdout ? decodeWindowsTextBytes(stdout) : "", + timedOut: errorCode === "ETIMEDOUT" || (error !== null && "killed" in error && error.killed === true), + spawnFailed: error !== null && typeof errorCode !== "number" && errorCode !== "ETIMEDOUT", + }); + }); +}); + function usable(result: ClaudeDesktopPolicyProbeResult): boolean { return !result.timedOut && !result.spawnFailed && result.status !== null; } @@ -108,6 +130,71 @@ export function probeClaudeDesktopPolicy( return parentListsPolicyKey(parent.stdout) ? "unknown" : "absent"; } +/** Non-blocking variant for the long-lived server request path. */ +export async function probeClaudeDesktopPolicyAsync( + options: Omit & { readonly run?: ClaudeDesktopPolicyAsyncProbeRunner } = {}, +): Promise { + const platform = options.platform ?? process.platform; + if (platform !== "win32") return "not_applicable"; + + let regExe: string; + try { + const systemDirectory = (options.resolveSystemDirectory ?? resolveTrustedWindowsSystemDirectory)(); + regExe = win32.join(systemDirectory, "reg.exe"); + } catch { + return "unknown"; + } + + const run = options.run ?? defaultAsyncPolicyProbeRunner; + try { + const policy = await run(regExe, ["query", CLAUDE_POLICY_KEY, "/reg:64"]); + if (!usable(policy)) return "unknown"; + if (policy.status === 0) return "present"; + if (policy.status !== 1) return "unknown"; + + const parent = await run(regExe, ["query", CLAUDE_POLICY_PARENT_KEY, "/reg:64"]); + if (!usable(parent) || parent.status !== 0) return "unknown"; + return parentListsPolicyKey(parent.stdout) ? "unknown" : "absent"; + } catch { + return "unknown"; + } +} + +const POLICY_CACHE_TTL_MS = 30_000; + +export function createCachedClaudeDesktopPolicyProbe( + probe: () => Promise, + ttlMs = POLICY_CACHE_TTL_MS, + now = Date.now, +): () => Promise { + let cached: { state: ClaudeDesktopPolicyState; expiresAt: number } | undefined; + let refresh: Promise | undefined; + return () => { + const currentTime = now(); + if (cached && cached.expiresAt > currentTime) return Promise.resolve(cached.state); + if (refresh) return refresh; + refresh = probe().then((state) => { + cached = { state, expiresAt: now() + ttlMs }; + return state; + }).finally(() => { + refresh = undefined; + }); + return refresh; + }; +} + +const cachedProductionProbe = createCachedClaudeDesktopPolicyProbe( + () => probeClaudeDesktopPolicyAsync(), +); + +/** Coalesces status polling and bounds registry refreshes to one per cache interval. */ +export function getCachedClaudeDesktopPolicy( + options: Omit = {}, +): Promise { + if (options.platform === undefined || options.platform === process.platform) return cachedProductionProbe(); + return probeClaudeDesktopPolicyAsync(options); +} + /** State-only health projection shared by CLI, apply, and management status. */ export function claudeDesktopPolicyHealth( state: ClaudeDesktopPolicyState, diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index d25e4895fb..ed94ee9702 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -983,10 +983,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise // a stale apply the operator should refresh. const stale = desiredEnabled && observed.kind === "gateway_drifted"; const { getDesktopHealth } = await import("../../claude/desktop-health"); - const { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy"); - const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({ - platform: deps.platform ?? process.platform, - }); + const { claudeDesktopPolicyHealth, getCachedClaudeDesktopPolicy } = await import("../../claude/desktop-policy"); + const policyState = deps.probeClaudeDesktopPolicy + ? deps.probeClaudeDesktopPolicy({ platform: deps.platform ?? process.platform }) + : await getCachedClaudeDesktopPolicy({ platform: deps.platform ?? process.platform }); const policy = claudeDesktopPolicyHealth(policyState); const health = { ...getDesktopHealth(), diff --git a/tests/claude-desktop-policy.test.ts b/tests/claude-desktop-policy.test.ts index 648c3f16dd..eae97a235f 100644 --- a/tests/claude-desktop-policy.test.ts +++ b/tests/claude-desktop-policy.test.ts @@ -1,6 +1,8 @@ import { expect, test } from "bun:test"; import { claudeDesktopPolicyHealth, + createCachedClaudeDesktopPolicyProbe, + probeClaudeDesktopPolicyAsync, probeClaudeDesktopPolicy, type ClaudeDesktopPolicyProbeRunner, } from "../src/claude/desktop-policy"; @@ -122,3 +124,40 @@ test("non-Windows policy probing is not applicable and never spawns", () => { expect(spawned).toBe(false); expect(resolved).toBe(false); }); + +test("the asynchronous policy probe does not synchronously block the caller", async () => { + let release!: (value: ReturnType) => void; + const pending = new Promise>((resolve) => { release = resolve; }); + const probe = probeClaudeDesktopPolicyAsync({ + platform: "win32", + resolveSystemDirectory: () => "C:\\trusted\\System32", + run: () => pending, + }); + + let settled = false; + void probe.then(() => { settled = true; }); + await Promise.resolve(); + expect(settled).toBe(false); + release(result({ status: 0 })); + expect(await probe).toBe("present"); +}); + +test("status policy probes coalesce concurrent refreshes and cache the result", async () => { + let calls = 0; + let clock = 0; + let release!: (state: "present") => void; + const pending = new Promise<"present">((resolve) => { release = resolve; }); + const cachedProbe = createCachedClaudeDesktopPolicyProbe(async () => { + calls += 1; + return pending; + }, 30_000, () => clock); + + const first = cachedProbe(); + const concurrent = cachedProbe(); + expect(calls).toBe(1); + release("present"); + expect(await Promise.all([first, concurrent])).toEqual(["present", "present"]); + clock = 29_999; + expect(await cachedProbe()).toBe("present"); + expect(calls).toBe(1); +});