import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js"; import { readClientPolicyBlock } from "./GeminiClientSettings.js"; import { readEffectiveClientRegistrationConfiguration } from "./ClientConfigurationFiles.js"; import { clientServerForcedEnabled, clientServerListedDisabled, effectiveClientServer, type ClientConfigurationDocument, } from "./ClientConfigurationDocument.js"; import { lstat, stat } from "node:fs/promises"; import { err, ok, type Result } from "../domain/result.js"; import { PRODUCT_IDENTITY } from "../identity.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js"; import { MCP_STARTUP_POLICY, mcpProviderDeadlineAllowanceMs, recommendedMcpToolTimeoutMs, recommendedMcpToolTimeoutSeconds, type McpToolTimeoutInputs, } from "../mcpStartupPolicy.js"; import { inspectClientRegistrationCommand, type ClientRegistrationCommandFacts, type ClientRegistrationCommandIdentity, } from "./ClientRegistrationIdentity.js"; import { clientEvidencePaths, clientOwnershipEvidencePaths, compactInputSchemaByDefault, manualRegistrationRemediation, supportedClients, } from "./SupportedClients.js"; import type { SetupClient } from "./SupportedClients.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { dshDocument } from "./DshCordisPatch.js"; import { clientRegistrationEnvironmentDetailsWithBudget, recordedProviderSettings, } from "./ClientRegistrationEnvironment.js"; import type { OwnedProviderSetting, ProviderRequestBudgetResolver, RecordedProviderSettings, } from "./ClientRegistrationEnvironment.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { parseClientRegistrationPolicy, parseClientRegistrationLauncher, clientRegistrationOwnershipRemediation, setupPreservesDisabledActivation, type ClientRegistration, } from "./ClientRegistration.js"; import { admitClientStdioConnection } from "./ClientRegistrationConnection.js"; interface ClientRegistrationStatusBase { readonly client: string; readonly config_path: string; /** Parsed launcher ownership; independent of readiness and retained without reinspection. */ readonly command_identity: ClientRegistrationCommandIdentity; /** Exact package reference requested by npx/bunx, when registration uses one. */ readonly package_reference?: string; /** Non-blocking timeout guidance; caller-selected values remain aligned. */ readonly advisory?: string; /** Provider settings this process selected that the registration's file does not carry. */ readonly provider_settings?: RecordedProviderSettings; } type RegistrationCommand = readonly [string, ...string[]]; export type ClientRegistrationStatus = ClientRegistrationStatusBase & ( | { readonly command: RegistrationCommand; readonly state: "aligned"; readonly remediation: null; } | { readonly command: RegistrationCommand; readonly state: "stale"; readonly remediation: string; } | { readonly command: RegistrationCommand; readonly state: "unknown"; readonly remediation: string; } | { readonly command: readonly []; readonly state: "missing" | "invalid" | "manual"; readonly remediation: string; } ); export type UnhealthyClientRegistrationStatus = Exclude< ClientRegistrationStatus, { readonly state: "aligned" } >; interface ClientRegistrationReadOptions { readonly platform?: NodeJS.Platform; readonly environment?: NodeJS.ProcessEnv; readonly resources?: ArtifactResourceScope; readonly selectedClientIds?: readonly string[]; /** * Which evidence authorises inspection. `readiness` inspects a client whose * product applicability is established, plus clients the caller selected * explicitly. `ownership` also inspects shared registration storage, because * a version-pinned registration REA wrote must be refreshed even when the * product profile is absent. */ readonly evidence?: "readiness" | "ownership"; readonly resolveProviderRequestBudget?: ProviderRequestBudgetResolver; /** Settings to compare with each registration's file; omitted skips that read. */ readonly providerSettings?: Readonly>; /** Which recorded provider settings hold a value the caller owns. */ readonly ownedProviderSetting?: OwnedProviderSetting; /** Inspect selected resolved settings without returning their values in status. */ readonly observeProviderSetting?: ( client: string, name: string, value: string, ) => void; } /** Registration facts shared by diagnostics and update maintenance. */ export interface ClientRegistrationInspection { readonly status: ClientRegistrationStatus; readonly activation: "enabled" | "disabled" | "unknown"; } /** Project diagnostics from the same inspection used to select maintenance. */ export const readClientRegistrationStatuses = async ( home: string, currentCommandPath: string | undefined = CURRENT_ENTRYPOINT_PATH, options: ClientRegistrationReadOptions = {}, ): Promise => ( await readClientRegistrationInspections(home, currentCommandPath, options) ).map(({ status }) => status); /** Parse effective registrations and their client-specific activation once. * A supplied scope retains failed cleanup; otherwise transfer its exact failure. */ export const readClientRegistrationInspections = async ( home: string, currentCommandPath: string | undefined = CURRENT_ENTRYPOINT_PATH, options: ClientRegistrationReadOptions = {}, ): Promise => { const inspections: ClientRegistrationInspection[] = []; const record = ( status: ClientRegistrationStatus, activation: ClientRegistrationInspection["activation"] = "unknown", ): void => { inspections.push({ status, activation }); }; for (const client of supportedClients( home, options.platform, options.environment, )) { const selected = options.selectedClientIds?.includes(client.name) === true; if (client.configPathError !== undefined) { record({ client: client.name, config_path: client.configPath, command: [], command_identity: "unknown", state: "invalid", remediation: client.configPathError, }); continue; } const presence = await clientEvidencePresent(client, { selected, ownership: options.evidence === "ownership", }); if (!presence.ok) { record({ client: client.name, config_path: presence.error.path, command: [], command_identity: "unknown", state: "invalid", remediation: presence.error.detail, }); continue; } if (!presence.value) continue; const manualRemediation = manualRegistrationRemediation(client.name); if (client.format === "unsupported") { if (manualRemediation !== undefined) record({ client: client.name, config_path: client.markerPath ?? client.configPath, command: [], command_identity: "unknown", state: "manual", remediation: manualRemediation, }); continue; } let selectedConfigPath = client.configPath; try { const configuration = await readEffectiveClientRegistrationConfiguration( client, options.resources, options.platform, ); if (!configuration.ok) { if ( options.resources === undefined && configuration.error.cleanupFailure !== undefined ) throw configuration.error.cleanupFailure; record({ client: client.name, config_path: configuration.error.path, command: [], command_identity: "unknown", state: "invalid", remediation: configuration.error.detail, }); continue; } const { document: parsed, configPath } = configuration.value; selectedConfigPath = configPath; if (parsed === undefined) { record(unavailableStatus(client, "missing", configPath)); continue; } const policy = await readClientPolicyBlock( client, parsed, undefined, options.resources, ); if (!policy.ok) { if ( options.resources === undefined && policy.error.cleanupFailure !== undefined ) throw policy.error.cleanupFailure; record({ client: client.name, config_path: configPath, command: [], command_identity: "unknown", state: "invalid", remediation: policy.error.detail, }); continue; } const policyBlock = policy.value; const raw = effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey); if (raw === undefined) { const editabilityError = parsed.dialect === "dsh" ? dshDocument(parsed.document).editabilityError : undefined; if (editabilityError !== undefined) { record({ client: client.name, config_path: configPath, command: [], command_identity: "unknown", state: "invalid", remediation: editabilityError.message, }); continue; } record(unavailableStatus(client, "missing", configPath)); continue; } const effectiveClient: SetupClient = parsed.dialect === "zcode_fallback" ? { ...client, configPath, configPaths: [configPath], format: "zcode_fallback", } : client; const parsedRegistration = parseClientRegistrationPolicy( raw, effectiveClient, ); if (!parsedRegistration.ok) { // Invalid readiness settings do not erase a usable launcher identity. const launcher = parseClientRegistrationLauncher(raw, { dialect: parsed.dialect, profile: client.registrationProfile, }); const commandIdentity = launcher.ok ? inspectClientRegistrationCommand( [launcher.value.command, ...launcher.value.args], currentCommandPath, ).identity : "unknown"; record({ client: client.name, config_path: configPath, command: [], command_identity: commandIdentity, state: "invalid", remediation: `${parsedRegistration.error.message}: ${parsedRegistration.error.settings.map(({ setting, constraint }) => `${setting}: ${constraint}`).join("; ")}`, }); continue; } const registration = parsedRegistration.value; const command: RegistrationCommand = [ registration.command, ...registration.args, ]; const commandFacts = inspectClientRegistrationCommand( command, currentCommandPath, ); const nativeApplicability = admitClientStdioConnection(raw, { dialect: parsed.dialect, profile: client.registrationProfile, }); if ( !nativeApplicability.ok && nativeApplicability.error.kind === "invalid" ) { record({ client: client.name, config_path: configPath, command: [], command_identity: commandFacts.identity, state: "invalid", remediation: nativeApplicability.error.cause.message, }); continue; } const registrationEnvironment = compactInputSchemaByDefault(client) || client.name === "codex" ? await clientRegistrationEnvironmentDetailsWithBudget( parsed, client, {}, "effective", client.name === "kimi" || client.name === "codex" ? options.resolveProviderRequestBudget : undefined, options.platform, ) : undefined; if ( registrationEnvironment !== undefined && !registrationEnvironment.ok ) { record({ client: client.name, config_path: configPath, command: [], command_identity: commandFacts.identity, state: "invalid", remediation: registrationEnvironment.error.message, }); continue; } const packageReference = commandFacts.package?.reference; const activation = registrationActivation(registration, client, parsed); const packageVersion = commandFacts.package?.exactVersion; const packageVersionDrift = packageVersion !== undefined && packageVersion !== PRODUCT_IDENTITY.packageVersion; const alignment = !nativeApplicability.ok ? "unknown" : registrationAligned({ registration, client: effectiveClient, commandFacts, inputSchemaProfileSelected: registrationEnvironment?.ok === true && snapshotEnvironment( registrationEnvironment.value.values, options.platform, ).REA_MCP_INPUT_SCHEMA_PROFILE !== undefined, }); record( configuredStatus( effectiveClient, command, commandFacts.identity, alignment === "unknown" ? "unknown" : activation === "enabled" && !packageVersionDrift && alignment === "aligned" && (parsed.dialect !== "dsh" || dshDocument(parsed.document).ready) && policyBlock === undefined ? "aligned" : "stale", (parsed.dialect !== "dsh" && commandFacts.identity !== "owned" ? clientRegistrationOwnershipRemediation(commandFacts.identity, [ configPath, ]) : undefined) ?? (!nativeApplicability.ok ? nativeApplicability.error.cause.message : undefined) ?? policyBlock ?? (packageVersionDrift ? packageVersionRemediation(packageReference ?? "", client) : alignment === "unknown" ? unknownEntryPointRemediation() : undefined) ?? // Only a target whose setup preserves the flag is a manual // conflict; a native row (for example ZCode's own storage) is // still repaired by its setup run below. (activation === "disabled" && registration.enable === false && setupPreservesDisabledActivation( effectiveClient, effectiveClient.format, ) ? sharedDisabledActivationRemediation() : undefined), packageReference, registrationEnvironment?.ok === true ? timeoutAdvisory( client, registration, registrationEnvironment.value.timeoutInputs, ) : undefined, options.providerSettings === undefined ? undefined : recordedProviderSettings( parsed, client, options.providerSettings, options.platform, options.ownedProviderSetting, options.observeProviderSetting === undefined ? undefined : (name, value) => options.observeProviderSetting!(client.name, name, value), ), ), activation, ); } catch (cause: unknown) { if ( options.resources === undefined && cause instanceof RegularFileCleanupFailure ) throw cause; record( unavailableStatus( client, isMissing(cause) ? "missing" : "invalid", selectedConfigPath, ), ); } } return inspections.sort((left, right) => compareUnicodeCodePoints(left.status.client, right.status.client), ); }; /** Shared registration storage doubles as ZCode's fallback, so its * `enable: false` activation fact binds every reader of that storage. * Maintenance must not drop the flag by rewriting the row as another * product's registration and silently re-enabling it; whether a repair * actually preserves it is setupPreservesDisabledActivation. */ const sharedEnableFlagBinds = ( registration: ClientRegistration, client: SetupClient, parsed: ClientConfigurationDocument, ): boolean => (parsed.dialect === "zcode" || parsed.dialect === "zcode_fallback" || client.sharedRegistration === true) && registration.enable === false; /** A disable that setup preserves is a manual conflict: its remediation * must not point at an ineffective setup run. */ const sharedDisabledActivationRemediation = (): string => "This shared registration row carries enable: false, which binds every reader of the shared storage, including ZCode's fallback view. REA preserves that flag, so rea setup will not change this row. Re-enable it with the product that disabled it, or remove enable: false manually, then rerun rea doctor."; // Codex and Kimi Code load a server marked `disabled: true` and honor only // `enabled`; Devin loads one marked `enabled: false` and honors only `disabled`. const ignoresDisabledKey = new Set(["codex", "kimi"]); const ignoresEnabledKey = new Set(["devin"]); const registrationActivation = ( registration: ClientRegistration, client: SetupClient, parsed: ClientConfigurationDocument, ): "enabled" | "disabled" => (client.format !== "grok" && !ignoresDisabledKey.has(client.name) && registration.disabled === true) || (registration.enabled === false && !ignoresEnabledKey.has(client.name) && !clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) || sharedEnableFlagBinds(registration, client, parsed) || clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey) ? "disabled" : "enabled"; const registrationAligned = ({ registration, client, commandFacts, inputSchemaProfileSelected, }: { readonly registration: ClientRegistration; readonly client: SetupClient; readonly commandFacts: ClientRegistrationCommandFacts; readonly inputSchemaProfileSelected: boolean; }): "aligned" | "stale" | "unknown" => { const { identity } = commandFacts; if (identity === "unknown") return "unknown"; if (identity === "foreign") return "stale"; if ( client.name === "grok_build" && registration.startup_timeout_sec === undefined ) return "stale"; if ( client.name === "codex" && (registration.startup_timeout_sec === undefined || registration.tool_timeout_sec === undefined) ) return "stale"; if ( client.name === "kimi" && (registration.startupTimeoutMs === undefined || registration.toolTimeoutMs === undefined) ) return "stale"; if (compactInputSchemaByDefault(client) && !inputSchemaProfileSelected) return "stale"; // Native transport is admitted separately; the Harness route must name REA. if (client.format === "dsh" && registration.serverName !== "rea") return "stale"; // Legacy package runners remain owned, but setup migrates them to the selected installation. if (commandFacts.package !== undefined) return "stale"; // Owning the script does not establish how its shebang interpreter resolves // in the client. Setup migrates these launchers without losing ownership. return commandFacts.launch === "runtime_script" ? "aligned" : "stale"; }; /** A client that rewrites its file from memory must be quit before setup. */ const remediation = (client: SetupClient): string => { if (client.quitBeforeSetup !== true) return "Run rea setup to refresh this registration, then restart the client."; const name = client.displayName ?? client.name; return `Quit ${name} fully, run rea setup to refresh this registration, then start ${name} again. A running ${name} can rewrite its configuration from memory and drop the registration.`; }; const unknownEntryPointRemediation = (): string => "The existing registration was retained because its executable identity cannot be verified. Check the registered launcher path, symlink targets and access permissions. Run the installed REA CLI or a supported package runner, then rerun rea doctor; no registration was changed."; const configuredStatus = ( client: SetupClient, command: RegistrationCommand, commandIdentity: ClientRegistrationCommandIdentity, state: "aligned" | "stale" | "unknown", policyRemediation?: string, packageReference?: string, advisory?: string, providerSettings?: RecordedProviderSettings, ): ClientRegistrationStatus => state === "aligned" ? { client: client.name, config_path: client.configPath, command, command_identity: commandIdentity, ...(packageReference === undefined ? {} : { package_reference: packageReference }), state, remediation: null, ...(advisory === undefined ? {} : { advisory }), ...(providerSettings === undefined ? {} : { provider_settings: providerSettings }), } : { client: client.name, config_path: client.configPath, command, command_identity: commandIdentity, ...(packageReference === undefined ? {} : { package_reference: packageReference }), state, remediation: policyRemediation ?? remediation(client), ...(providerSettings === undefined ? {} : { provider_settings: providerSettings }), }; const timeoutAdvisory = ( client: SetupClient, registration: ClientRegistration, timeoutInputs: McpToolTimeoutInputs, ): string | undefined => { const toolTimeout = client.name === "kimi" ? registration.toolTimeoutMs : client.name === "codex" ? registration.tool_timeout_sec : undefined; if (toolTimeout === undefined) return undefined; const providerAllowance = mcpProviderDeadlineAllowanceMs(timeoutInputs); const isKimi = client.name === "kimi"; // A bigint is beyond the safe Number range, so rounding cannot cross the allowance. const toolTimeoutMs = Number(toolTimeout) * (isKimi ? 1 : 1000); const batchScope = timeoutInputs.ghidraDecompileTimeoutMs !== undefined && (timeoutInputs.analysisProvider === "ghidra" || timeoutInputs.analysisProvider === "auto") ? "The derived Ghidra deadline is a single-decompile estimate. For multi-function workflows such as batch_decompile, decompile_all or trace_native_values, allow startup plus every attempted function's limit and other work; split calls, reduce trace_native_values max_functions, or select a larger client deadline when needed. Explicit caller deadlines remain unchanged." : undefined; if (toolTimeoutMs >= providerAllowance) return batchScope; const setting = isKimi ? "toolTimeoutMs" : "tool_timeout_sec"; const currentValue = isKimi ? `${String(toolTimeout)} ms` : `${String(toolTimeout)} seconds`; const recommendedValue = isKimi ? `${String(recommendedMcpToolTimeoutMs(timeoutInputs))} ms` : `${String(recommendedMcpToolTimeoutSeconds(timeoutInputs))} seconds`; const headroom = isKimi ? `${String(MCP_STARTUP_POLICY.toolOperationHeadroomMs)} ms` : `${String(MCP_STARTUP_POLICY.toolOperationHeadroomMs / 1000)} seconds`; const recommendation = `The configured ${setting} (${currentValue}) is shorter than REA's known provider allowance (${String(providerAllowance)} ms), so provider startup or an upstream request may exceed this client deadline. REA recommends ${setting} = ${recommendedValue} to cover that allowance plus ${headroom} of operation headroom; this additional headroom is a recommendation, not a guarantee for every analysis.`; return batchScope === undefined ? recommendation : `${recommendation} ${batchScope}`; }; const packageVersionRemediation = ( packageReference: string, client: SetupClient, ): string => `This registration requests ${packageReference}, while the current REA CLI package is ${PRODUCT_IDENTITY.packageVersion}. REA does not resolve package-runner requests during doctor; update the registration with rea setup --client ${client.name}, then restart the client.`; const unavailableStatus = ( client: SetupClient, state: "missing" | "invalid", configPath: string, ): ClientRegistrationStatus => ({ client: client.name, config_path: configPath, command: [], command_identity: "unknown", state, remediation: remediation(client), }); /** Product discovery follows targets; configuration evidence retains entry presence. */ const clientEvidencePresent = async ( client: SetupClient, evidence: { readonly selected: boolean; readonly ownership: boolean }, ): Promise< Result > => { // An explicit selection authorises the read outright: the caller named this // client, so an unreadable config must surface as unreadable instead of // collapsing to absence. The probes below serve automatic discovery only. if (evidence.selected) return ok(true); const discoveryPaths = new Set(client.discoveryPaths ?? []); const paths = [ ...clientEvidencePaths(client), ...(evidence.ownership ? clientOwnershipEvidencePaths(client) : []), ]; let unavailable: | { readonly path: string; readonly detail: string } | undefined; for (const path of paths) { try { await (discoveryPaths.has(path) ? stat(path) : lstat(path)); return ok(true); } catch (cause: unknown) { if (!isMissing(cause)) unavailable ??= { path, detail: `Client configuration evidence at ${path} could not be inspected: ${cause instanceof Error ? cause.message : String(cause)}. Check file permissions and rerun rea doctor.`, }; } } return unavailable === undefined ? ok(false) : err(unavailable); }; const isMissing = (cause: unknown): boolean => cause instanceof Error && "code" in cause && cause.code === "ENOENT";