diff --git a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md index c65dca996d..491ea89289 100644 --- a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md +++ b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md @@ -128,7 +128,7 @@ rea setup --client codex --dry-run --json Use the current supported client's ID: `claude_code`, `claude_desktop`, `codex`, `cursor`, `gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`, `copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `trae`, `grok_build`, `omp`, `pi`, -`hermes`, `qoder`, `droid`, `zcode`, `dsh`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a +`hermes`, `qoder`, `droid`, `zcode`, `dsh`, `freebuff`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a scope. `grok_bot` uses a chat registration step rather than local `mcp.json`. For unsupported clients, use version-pinned manual stdio registration or CLI. diff --git a/docs/client-configuration.md b/docs/client-configuration.md index d86a296f6c..5d967061eb 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -504,6 +504,28 @@ as stale, and rerunning setup removes the flag to repair it. Native configuratio uses strict JSON; setup refuses comments, trailing commas and a BOM rather than writing a registration ZCode cannot load. +## FreeBuff + +For FreeBuff, the proposed integration writes `mcpServers.rea` into the user-scope +`~/.agents/mcp.json` shared-agent file. Entries carry `command`, `args`, and +`env`; stdio is the default transport. The proposed personal skill location is +`~/.agents/skills`, so setup installs its skill copy there. Because the same +file doubles as ZCode's user-scope fallback (see above), a `freebuff` +registration is also in the file ZCode uses until ZCode defines its own native server. +Setup and doctor detect FreeBuff from its own profile directory +(`~/.config/freebuff-desktop`), not from the shared `.agents` folder that +REA's skill installs create for every client. + +The configuration write and personal skill locations are proposed integration +boundaries. FreeBuff MCP loading and skill discovery have not been verified +with an installed native FreeBuff client/version; filesystem tests cover REA's +editing, discovery, and ownership behavior only. Automatic applicability +requires the FreeBuff profile directory even when the shared MCP file exists. + +FreeBuff is main-only and is not in a published release yet; npm 6.7.0 is the +latest. `rea setup --client freebuff` against `@latest` reports an unsupported +client until a release containing it is published. + ## DeepSeek Harness DeepSeek Harness registers MCP clients through a Cordis YAML patch sequence diff --git a/docs/client-verification.md b/docs/client-verification.md index 6561752e74..b72aedbe63 100644 --- a/docs/client-verification.md +++ b/docs/client-verification.md @@ -1,5 +1,10 @@ # Client and agent verification +FreeBuff MCP loading and personal skill discovery are currently unverified with +an installed native client/version. Its temporary-filesystem coverage verifies +REA's configuration and applicability boundaries only; it does not establish +that FreeBuff consumes the written registration or skill. + Use these optional lanes to verify an installed client or evaluate an agent's REA workflow. [Testing REA](testing.md) provides the normal development commands and provider lanes. Each record below states its host, client version, model diff --git a/docs/installation.md b/docs/installation.md index 62507fec5a..a8ff50e74c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -616,9 +616,12 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s | WorkBuddy | `workbuddy` | | ZCode | `zcode` | | DeepSeek Harness | `dsh` | +| FreeBuff | `freebuff` | `droid`, `dsh`, `kimi`, `qoder`, `trae`, `workbuddy`, and `zcode` require `rea-agents@6.5.0` or later. The 6.4.0 registry check above does not cover them. +`freebuff` is not in a release yet - `6.7.0` is the latest - so it also needs a +release containing it. See [client configuration and troubleshooting](client-configuration.md) for configuration paths, profiles, trust policies, and schema-limit workarounds. diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts index f4677370ad..e92cb0dbbc 100644 --- a/src/adb/AdbProvider.ts +++ b/src/adb/AdbProvider.ts @@ -18,15 +18,13 @@ import { } from "node:fs/promises"; import { basename, dirname, extname, join, resolve } from "node:path"; -import type { - ExecutionOptions, - ProviderAvailability, -} from "../application/AnalysisProvider.js"; -import { providerConfigurationRefusal } from "../domain/providerConfigurationRefusal.js"; import { + type ExecutionOptions, + type ProviderAvailability, createAnalysisExecution, type AnalysisExecution, } from "../application/AnalysisProvider.js"; +import { providerConfigurationRefusal } from "../domain/providerConfigurationRefusal.js"; import type { AdbDisplayNumericIssues, AdbDisplayPartialObservation, diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts index 40346ef531..9e46106f32 100644 --- a/src/apktool/ApktoolProvider.ts +++ b/src/apktool/ApktoolProvider.ts @@ -20,15 +20,13 @@ import { randomUUID } from "node:crypto"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; -import type { - ExecutionOptions, - ProviderAvailability, -} from "../application/AnalysisProvider.js"; -import { providerConfigurationRefusal } from "../domain/providerConfigurationRefusal.js"; import { + type ExecutionOptions, + type ProviderAvailability, createAnalysisExecution, type AnalysisExecution, } from "../application/AnalysisProvider.js"; +import { providerConfigurationRefusal } from "../domain/providerConfigurationRefusal.js"; import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js"; import { apktoolResultSchemas, diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index c7a9dd7c9c..4825d972c0 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -31,7 +31,12 @@ import { type StableRegularFileDescriptor, } from "../filesystem/RegularFile.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; -import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; +import type { + BinaryTarget, + BinaryArchitecture, + PeCodeViewRecord, + UniversalMachOSlice, +} from "../domain/binaryTargetTypes.js"; import { err, ok, type Result } from "../domain/result.js"; import { readMipsElfMetadata } from "./MipsElfMetadata.js"; import { validateElfDeclaredRanges } from "../domain/elfDeclaredRanges.js"; @@ -49,7 +54,6 @@ import { } from "../domain/rawBinary.js"; import { admitGamecubeDol } from "../domain/gamecubeDolAdmission.js"; import { DOL_HEADER_SIZE } from "../domain/gamecubeDol.js"; -import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; import { mzWindowsHeaderLayoutLimitation, observeMzWindowsHeader, @@ -96,10 +100,6 @@ import { readMachoLoadCommandTableHeader, type MachoLoadCommandTableHeader, } from "../domain/apple/machoLoadCommands.js"; -import type { - PeCodeViewRecord, - UniversalMachOSlice, -} from "../domain/binaryTargetTypes.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import type { JsonValue } from "../domain/jsonValue.js"; diff --git a/src/application/BrowserObservationService.test.ts b/src/application/BrowserObservationService.test.ts index b87c4f2a4d..d38d1e1d1e 100644 --- a/src/application/BrowserObservationService.test.ts +++ b/src/application/BrowserObservationService.test.ts @@ -1,6 +1,10 @@ import { expect, it } from "vitest"; import type { BrowserObservationPort } from "./BrowserObservationPort.js"; -import { listBrowserTargets } from "./BrowserObservationService.js"; +import { + listBrowserTargets, + inspectWebPage, + analyzeWebBundle, +} from "./BrowserObservationService.js"; import type { BrowserTargetList } from "../domain/browserObservationSchemas.js"; import { parseEvidence } from "../domain/evidence.js"; import { @@ -18,10 +22,8 @@ import { BrowserObservationError } from "../domain/browserObservationError.js"; import { err, ok } from "../domain/result.js"; import { passiveNetworkCapture } from "../../tests/fixtures/passiveNetworkCapture.js"; import { analyzeWebBundleFixture } from "../../tests/support/browser/analyzeWebBundleFixture.js"; -import { inspectWebPage } from "./BrowserObservationService.js"; import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js"; import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; -import { analyzeWebBundle } from "./BrowserObservationService.js"; const input = { cdp_endpoint: { diff --git a/src/application/ClientRegistration.ts b/src/application/ClientRegistration.ts index ec38c6d7c1..caa1e1a28f 100644 --- a/src/application/ClientRegistration.ts +++ b/src/application/ClientRegistration.ts @@ -196,6 +196,18 @@ const managedConnectionKeys = new Set([ "httpUrl", ]); +/** Whether a rea setup repair for this target preserves the row's + * `enable: false` instead of dropping it. Shared registration doubles as + * ZCode's fallback, so the activation fact belongs to every reader; a repair + * preserves it instead of silently re-enabling a row it does not own. A + * client's own native storage has a single reader, so an explicitly selected + * setup may still repair it. Doctor's remediation reads this same rule so a + * preserved disable never points at an ineffective setup run. */ +export const setupPreservesDisabledActivation = ( + client: Pick, + format: string | undefined, +): boolean => client.sharedRegistration === true || format === "zcode_fallback"; + /** Patch a source fragment, retaining client-owned settings rather than rebuilding it. */ export const refreshClientRegistration = ( client: SetupClient, @@ -226,6 +238,11 @@ export const refreshClientRegistration = ( ), ), }; + if ( + setupPreservesDisabledActivation(client, dialect) && + existing.enable === false + ) + registration.enable = false; if (client.name === "codex" || client.name === "grok_build") registration.startup_timeout_sec = (client.name === "codex" diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts index 89093a411b..6f6bbff0c1 100644 --- a/src/application/ClientRegistrationStatus.ts +++ b/src/application/ClientRegistrationStatus.ts @@ -7,7 +7,7 @@ import { effectiveClientServer, type ClientConfigurationDocument, } from "./ClientConfigurationDocument.js"; -import { lstat } from "node:fs/promises"; +import { lstat, stat } from "node:fs/promises"; import { err, ok, type Result } from "../domain/result.js"; import { isAbsolute } from "node:path"; @@ -29,6 +29,7 @@ import { } from "./ClientRegistrationIdentity.js"; import { clientEvidencePaths, + clientOwnershipEvidencePaths, manualRegistrationRemediation, supportedClients, } from "./SupportedClients.js"; @@ -41,6 +42,7 @@ import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.j import { parseClientRegistration, + setupPreservesDisabledActivation, type ClientRegistration, } from "./ClientRegistration.js"; @@ -88,6 +90,15 @@ 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; } @@ -130,6 +141,7 @@ export const readClientRegistrationInspections = async ( options.platform, options.environment, )) { + const selected = options.selectedClientIds?.includes(client.name) === true; if (client.configPathError !== undefined) { record({ client: client.name, @@ -140,7 +152,10 @@ export const readClientRegistrationInspections = async ( }); continue; } - const presence = await clientEvidencePresent(client); + const presence = await clientEvidencePresent(client, { + selected, + ownership: options.evidence === "ownership", + }); if (!presence.ok) { record({ client: client.name, @@ -323,7 +338,18 @@ export const readClientRegistrationInspections = async ( ? packageVersionRemediation(packageReference ?? "", client) : alignment === "unknown" ? unknownEntryPointRemediation() - : undefined), + : 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( @@ -356,6 +382,25 @@ export const readClientRegistrationInspections = async ( ); }; +/** 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"]); @@ -372,8 +417,7 @@ const registrationActivation = ( (registration.enabled === false && !ignoresEnabledKey.has(client.name) && !clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) || - ((parsed.dialect === "zcode" || parsed.dialect === "zcode_fallback") && - registration.enable === false) || + sharedEnableFlagBinds(registration, client, parsed) || clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey) ? "disabled" : "enabled"; @@ -538,18 +582,28 @@ const unavailableStatus = ( remediation: remediation(client), }); -/** Entry presence is independent of whether its target can currently be read. */ +/** 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 clientEvidencePaths(client)) { + for (const path of paths) { try { - await lstat(path); + await (discoveryPaths.has(path) ? stat(path) : lstat(path)); return ok(true); } catch (cause: unknown) { if (!isMissing(cause)) diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 462d0a7100..0c16d806ad 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -1,6 +1,9 @@ import type { ExecutableFormatHint } from "../domain/dosCom.js"; import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js"; -import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; +import type { + BinaryArchitecture, + BinaryTarget, +} from "../domain/binaryTargetTypes.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; import type { BinarySession } from "./binary/BinarySession.js"; @@ -31,7 +34,6 @@ import { access, stat } from "node:fs/promises"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { requiresNativeSourceAdmissionBeforeReplay } from "./binary/AnalysisSnapshotCache.js"; import { classifyBinaryTargetFilesystemFailure } from "./BinaryTargetResolver.js"; -import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { snapshotEvidenceForQuery, snapshotMatchesTarget, diff --git a/src/application/Doctor.ts b/src/application/Doctor.ts index 2f55203220..26e0e66fd2 100644 --- a/src/application/Doctor.ts +++ b/src/application/Doctor.ts @@ -141,7 +141,9 @@ export interface DoctorHost { clientIds: readonly string[] | undefined, ): Promise; close?(): Promise; - clientRegistrations?(): Promise; + clientRegistrations?( + selectedClientIds?: readonly string[], + ): Promise; ilspyCmdVersion?(path: string): Promise; } @@ -369,7 +371,7 @@ const collectDoctorIdentity = async ( readonly value: DoctorIdentity; }> => { const installationPaths = (await host.installationPaths?.()) ?? []; - const registrations = (await host.clientRegistrations?.()) ?? []; + const registrations = (await host.clientRegistrations?.(scope.clients)) ?? []; const skillClientIds = scope.skill_clients ?? (scope.clients.length > 0 ? scope.clients : undefined); @@ -742,11 +744,12 @@ export const systemDoctorHost = ( throw failure; } }, - clientRegistrations: () => + clientRegistrations: (selectedClientIds) => readClientRegistrationStatuses(homeDirectory, undefined, { platform, environment, resources, + ...(selectedClientIds === undefined ? {} : { selectedClientIds }), ...(resolveProviderRequestBudget === undefined ? {} : { diff --git a/src/application/EnhancedTools.objcClasses.test.ts b/src/application/EnhancedTools.objcClasses.test.ts index f6371b9eb6..3093fa6bbf 100644 --- a/src/application/EnhancedTools.objcClasses.test.ts +++ b/src/application/EnhancedTools.objcClasses.test.ts @@ -10,8 +10,8 @@ import { EnhancedTools } from "./EnhancedTools.js"; import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; import { objcSwiftMetadataSchema } from "../domain/native/objcSwiftMetadata.js"; import { err, ok } from "../domain/result.js"; -import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; import { + AnalysisCapabilityUnavailableError, AnalysisOutputError, AnalysisCancelledError, } from "../domain/analysisErrorCore.js"; diff --git a/src/application/Setup.fixture.ts b/src/application/Setup.fixture.ts index 4f42035eb0..31c6a4e702 100644 --- a/src/application/Setup.fixture.ts +++ b/src/application/Setup.fixture.ts @@ -1,4 +1,4 @@ -import { skillDestinations } from "./SetupSkill.js"; +import { skillDestinations, SetupSkillInstallError } from "./SetupSkill.js"; import type { ClientConfigurationInspection, ClientConfigurationResult, @@ -9,7 +9,6 @@ import type { import type { SetupClient } from "./SupportedClients.js"; import type { SetupHopperInstallResult } from "./SetupInstallFailure.js"; import { err, ok, type Result } from "../domain/result.js"; -import { SetupSkillInstallError } from "./SetupSkill.js"; import type { DoctorCheck, DoctorProviderInspection, diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts index ddfe19e9f0..79ef773826 100644 --- a/src/application/SetupHost.ts +++ b/src/application/SetupHost.ts @@ -23,7 +23,12 @@ import { SUPPORTED_NODE_VERSION_PROSE, supportsNodeVersion, } from "../domain/runtimeVersion.js"; -import { runDoctor, systemDoctorHost, type DoctorHost } from "./Doctor.js"; +import { + runDoctor, + systemDoctorHost, + type DoctorHost, + type DoctorScope, +} from "./Doctor.js"; import { installLinuxHopper, LinuxHopperTemporaryCleanupOwner, @@ -67,7 +72,6 @@ import type { SetupInitialState, SetupProviderEnvironment, } from "./SetupTypes.js"; -import type { DoctorScope } from "./Doctor.js"; import { SetupCleanupOwner } from "./SetupCleanupOwner.js"; import { AnalysisCleanupLeaseGroup, diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts index d2e7251eab..460126c34e 100644 --- a/src/application/SupportedClients.ts +++ b/src/application/SupportedClients.ts @@ -34,10 +34,18 @@ export interface SetupClient { /** Same-scope fallback ZCode stops loading after its first native server. */ readonly zcodeFallbackPath?: string; readonly markerPath?: string; + /** + * Registration storage shared with other products. Its presence never proves + * this product is installed; only a recognizable REA-owned row in it proves + * ownership of a registration REA wrote. + */ + readonly sharedRegistration?: boolean; /** MSIX package storage that decides which configuration the client reads. */ readonly msixPackage?: MsixPackageView; /** A running client can rewrite its configuration from memory. */ readonly quitBeforeSetup?: boolean; + /** Product-specific applicability evidence when configuration storage is shared. */ + readonly discoveryPaths?: readonly string[]; readonly format?: | "json" | "claude_code" @@ -100,6 +108,8 @@ type ClientDefinition = ClientDefinitionBase & interface ClientDefinitionBase { readonly name: string; readonly displayName: string; + readonly discoveryPaths?: (context: ClientPathContext) => readonly string[]; + readonly sharedRegistration?: true; readonly format: NonNullable; readonly quitBeforeSetup?: boolean; readonly zcodeFallbackPath?: (context: ClientPathContext) => string; @@ -482,6 +492,15 @@ const devinDirectory = ({ home, platform, env }: ClientPathContext): string => ? join(windowsAppDataRoots(home, env).roaming, "devin") : join(env.XDG_CONFIG_HOME || join(home, ".config"), "devin"); +/** + * FreeBuff's own profile directory (its `state.json` root, fixed under + * `.config` on every platform). The shared `~/.agents` folder is not + * FreeBuff-specific: REA's own skill installs create it, so it cannot + * prove FreeBuff is installed. + */ +const freebuffProfileDirectory = ({ home }: ClientPathContext): string => + join(home, ".config", "freebuff-desktop"); + /** Stable product metadata used to derive setup discovery and documentation. */ export const SUPPORTED_CLIENT_DEFINITIONS = [ { @@ -683,6 +702,9 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [ configPath: [".zcode", "cli", "config.json"], markerPath: [".zcode", "cli"], skillPath: [".zcode", "skills"], + discoveryPaths: ({ home }: ClientPathContext) => [ + join(home, ".zcode", "cli"), + ], format: "zcode", }, { @@ -696,6 +718,23 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [ markerPath: dshHomeDirectory, format: "dsh", }, + { + name: "freebuff", + displayName: "FreeBuff", + // FreeBuff discovers personal skills in the shared user agents directory. + skillPath: ({ home }: ClientPathContext) => join(home, ".agents", "skills"), + // FreeBuff loads MCP servers from the shared user agents directory. + configPath: ({ home }: ClientPathContext) => + join(home, ".agents", "mcp.json"), + markerPath: freebuffProfileDirectory, + discoveryPaths: (context: ClientPathContext) => [ + freebuffProfileDirectory(context), + ], + // The same file doubles as ZCode's user-scope fallback, so its presence + // proves neither product is installed; see clientOwnershipEvidencePaths. + sharedRegistration: true, + format: "json", + }, { name: "grok_bot", displayName: "Grok Bot", @@ -706,22 +745,67 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [ ] as const satisfies readonly ClientDefinition[]; /** - * Paths whose presence means doctor and setup should inspect the client. - * Layered sources count: OpenCode still loads its XDG files when a custom - * directory override does not exist. + * Paths whose presence means the product itself is applicable, so doctor and + * setup should inspect the client. Layered sources count: OpenCode still loads + * its XDG files when a custom directory override does not exist. Shared + * registration storage never establishes applicability; see + * {@link clientOwnershipEvidencePaths}. */ export const clientEvidencePaths = ( - client: Pick, + client: Pick< + SetupClient, + "configPath" | "configPaths" | "markerPath" | "discoveryPaths" + >, ): readonly string[] => [ ...new Set( - [ - client.configPath, - ...(client.configPaths ?? []), - client.markerPath, - ].filter((path): path is string => path !== undefined && path.length > 0), + ( + client.discoveryPaths ?? [ + client.configPath, + ...(client.configPaths ?? []), + client.markerPath, + ] + ).filter((path): path is string => path !== undefined && path.length > 0), ), ]; +/** + * Paths where a recognizable REA-owned row proves ownership of a registration + * REA wrote, never that the product is installed: shared storage is created by + * other products and by REA's own skill installs. A client whose registration + * is product-local declares no shared storage, so this stays empty for it. + */ +export const clientOwnershipEvidencePaths = ( + client: Pick< + SetupClient, + "configPath" | "configPaths" | "sharedRegistration" + >, +): readonly string[] => + client.sharedRegistration === true + ? [client.configPath, ...(client.configPaths ?? [])].filter( + (path): path is string => path !== undefined && path.length > 0, + ) + : []; + +/** + * Registration storage this client shares with another product: its own shared + * configuration plus any same-scope fallback another client also loads. Only + * this storage may be retained when product applicability is unproven, because + * failing closed on it would block REA removals for files REA does not own. + * Product-local configuration is REA's own boundary and stays fail-closed. + */ +export const clientSharedRegistrationPaths = ( + client: Pick< + SetupClient, + "configPath" | "configPaths" | "sharedRegistration" | "zcodeFallbackPath" + >, +): ReadonlySet => + new Set([ + ...clientOwnershipEvidencePaths(client), + ...(client.zcodeFallbackPath === undefined + ? [] + : [client.zcodeFallbackPath]), + ]); + const resolvePath = ( path: ClientPath, context: ClientPathContext, @@ -786,6 +870,12 @@ export const supportedClients = ( ...(definition.quitBeforeSetup === true ? { quitBeforeSetup: true } : {}), + ...(definition.discoveryPaths === undefined + ? {} + : { discoveryPaths: definition.discoveryPaths(context) }), + ...(definition.sharedRegistration === true + ? { sharedRegistration: true } + : {}), format: definition.format, }; } catch (cause: unknown) { diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts index 9db087b5d7..c76169bbe7 100644 --- a/src/application/Uninstall.ts +++ b/src/application/Uninstall.ts @@ -4,6 +4,7 @@ import { } from "./ClientConfigurationCleanup.js"; import { ClientConfigurationShapeError, + clientConfigurationServersKey, parseClientConfiguration, serializeClientConfiguration, clientServerPath, @@ -47,6 +48,8 @@ import { import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { appDataRedirection, + clientOwnershipEvidencePaths, + clientSharedRegistrationPaths, manualRegistrationRemediation, registrationWriteBlocker, supportedClients, @@ -121,12 +124,20 @@ export interface UninstallResult { readonly items: readonly UninstallItem[]; } +/** Applicability established once from product-specific discovery evidence. */ +export interface UninstallClient extends SetupClient { + readonly applicability?: "unverified"; + /** Clients that load this same registration table as a fallback report the + * owner's outcome without inspecting or editing the file again. */ + readonly sharedWith?: readonly string[]; +} + /** Filesystem boundary used by the contained uninstall workflow. */ export interface UninstallHost { - clients(): Promise; + clients(): Promise; /** Report a configuration that cannot be read safely, before any removal. */ - inspectClient(client: SetupClient): Promise; - removeClient(client: SetupClient): Promise; + inspectClient(client: UninstallClient): Promise; + removeClient(client: UninstallClient): Promise; inspectSkill?(): Promise; removeSkill(): Promise; purgeData(): Promise; @@ -172,7 +183,9 @@ export const runUninstall = async ( status: "retained", detail: "Hopper is not owned by REA uninstall.", }); - const reported = collapseUninstallItems(items); + const reported = collapseUninstallItems( + projectSharedRegistrations(items, clients), + ); result = { status: reported.some(({ status }) => status === "failed") ? "failed" @@ -213,7 +226,7 @@ export const runUninstall = async ( * overwritten. A client that fails then still stops the remaining removals. */ const removeAll = async ( - clients: readonly SetupClient[], + clients: readonly UninstallClient[], purgeData: boolean, host: UninstallHost, ): Promise => { @@ -236,6 +249,189 @@ const removeAll = async ( return items; }; +/** + * Report a shared file's single outcome under every client that loads it. The + * owner edits the file once, so a fallback reader reports what happened to its + * registration instead of finding it already absent. + */ +const projectSharedRegistrations = ( + items: readonly UninstallItem[], + clients: readonly UninstallClient[], +): UninstallItem[] => { + const readersByRegistration = new Map(); + for (const client of clients) + if (client.sharedWith !== undefined && client.sharedWith.length > 0) + readersByRegistration.set( + JSON.stringify([client.name, client.configPath]), + client.sharedWith, + ); + if (readersByRegistration.size === 0) return [...items]; + return items.flatMap((next) => { + const readers = + next.configuration_path === undefined + ? undefined + : readersByRegistration.get( + JSON.stringify([next.name, next.configuration_path]), + ); + if (readers === undefined) return [next]; + const { name, ...action } = next; + return [name, ...readers].map((reader) => ({ ...action, name: reader })); + }); +}; + +/** + * Ownership is a readable, recognizable REA-owned row, never the shared + * file's existence. An unrelated malformed file must not establish ownership; + * it stays retained without blocking other removals. + */ +const hasOwnershipEvidence = async ( + client: UninstallClient, + fileSystem: UninstallFileSystem, +): Promise => { + for (const path of clientOwnershipEvidencePaths(client)) { + let original: string; + try { + original = await fileSystem.readText(path); + } catch (cause: unknown) { + // A close failure after a completed read still carries the exact text + // in its retained outcome, and that text is the ownership fact: only a + // read that never completed proves nothing. Preserve it, so later read + // failures stay blocking instead of softening to retained. + if (!(cause instanceof RegularFileCleanupFailure)) continue; + if (cause.outcome.kind !== "completed") continue; + const completed: unknown = cause.outcome.value; + if (typeof completed !== "string") continue; + original = completed; + } + let parsed: ClientConfigurationDocument; + try { + parsed = parseClientConfiguration(original, client.format); + } catch { + // Malformed shared data is retained, never ownership evidence. + continue; + } + const removal = clientRegistrationRemoval(parsed, original, client); + // Refusing the edit never retracts the owned-row fact: an effective + // REA-owned row shadowed by a foreign duplicate still proves ownership, + // so later read failures stay blocking instead of softening to retained. + const owned = removal.ok ? removal.value : removal.error.owned; + if (owned.removeNative || owned.legacyPath !== undefined) return true; + } + return false; +}; + +/** Installation evidence for one probed path, never inferred from absence. */ +interface DiscoveryProbe { + readonly applicable: boolean; + readonly unresolved: boolean; +} + +/** A client's shared-storage applicability from its discovery paths: a hit + * proves installation, a miss leaves it unproven, and any other probe failure + * leaves it unresolved — never absence. */ +const discoveryApplicability = async ( + discoveryPaths: readonly string[] | undefined, + fileSystem: UninstallFileSystem, +): Promise => { + if (discoveryPaths === undefined) + return { applicable: true, unresolved: false }; + let applicable = false; + let unresolved = false; + for (const path of discoveryPaths) { + try { + // The product's own directory proves installation, not a leftover link + // to it: `stat` is `lstat`, so a dangling profile link passes here even + // though setup and doctor, which follow the target, reject it. Resolve + // the link for the same answer, keeping `lstat` for removal safety. + const stats = await fileSystem.stat(path); + if (stats.isSymbolicLink() && fileSystem.realpath !== undefined) + await fileSystem.realpath(path); + return { applicable: true, unresolved: false }; + } catch (cause: unknown) { + // Absence keeps scanning until one discovery path exists; any + // other probe failure leaves applicability unresolved. + if (!isMissing(cause)) unresolved = true; + } + } + return { applicable, unresolved }; +}; + +/** One physical configuration path a client claims. */ +interface ClientConfigClaim { + readonly client: SetupClient; + readonly configPath: string; + /** The path is another product's storage this client falls back to. */ + readonly fallback: boolean; +} + +/** One physical file, the client whose configuration it is, and the other + * clients that load it as a fallback. */ +interface SharedConfigPath { + readonly owner: ClientConfigClaim; + readonly readers: readonly ClientConfigClaim[]; +} + +/** + * Resolve each physical configuration file to the clients that edit it. A + * client's own configuration owns its registration row; a fallback view only + * reports the owner's decision instead of racing it to "absent". Two clients + * that each declare the same path as their own configuration keep their own + * row, because they write different tables. + */ +const sharedConfigPaths = ( + clients: readonly SetupClient[], +): readonly SharedConfigPath[] => { + const claimsByPath = new Map(); + for (const client of clients) + for (const claim of [ + ...uninstallConfigPaths(client).map((configPath) => ({ + client, + configPath, + fallback: false, + })), + ...(client.zcodeFallbackPath === undefined + ? [] + : [ + { + client, + configPath: client.zcodeFallbackPath, + fallback: true, + }, + ]), + ]) + claimsByPath.set(claim.configPath, [ + ...(claimsByPath.get(claim.configPath) ?? []), + claim, + ]); + return [...claimsByPath.values()].flatMap((claims): SharedConfigPath[] => { + const fallbacks = claims.filter((claim) => claim.fallback); + const owners = claims.filter((claim) => !claim.fallback); + // A fallback view can only report one client's row. OpenCode can be pointed + // at FreeBuff's file through `OPENCODE_CONFIG`, and both then own the path + // while writing different tables, so each keeps its own decision. + if (owners.length > 0) + return owners.map((owner) => { + const format = owner.client.format; + // ZCode's fallback reads mcpServers, never another table in the same + // file (for example OpenCode's mcp table). Use the format owner's + // mapping instead of reinterpreting table names here. + const readers = + format !== undefined && + format !== "unsupported" && + format !== "dsh" && + clientConfigurationServersKey(format) === + clientConfigurationServersKey("zcode_fallback") + ? fallbacks + : []; + return { owner, readers }; + }); + const [only] = fallbacks; + if (only === undefined) + throw new Error("A configuration path has no claiming client."); + return [{ owner: only, readers: [] }]; + }); +}; + /** Create uninstall effects contained to detected client configs and REA-owned paths. */ export const systemUninstallHost = ( selectedHome: string | undefined = undefined, @@ -250,6 +446,7 @@ export const systemUninstallHost = ( // a peer's pending cleanup otherwise waits for the operation requesting it. const configurationOperations = new ArtifactResourceScope(); const fileSystem = selectedFileSystem ?? systemFileSystem(resources); + const preflightOwnedClients = new WeakSet(); // Set with the client list: the package view that redirects this // process's AppData writes, if any. let redirection: MsixPackageView | undefined; @@ -259,33 +456,79 @@ export const systemUninstallHost = ( const skillWriteBlocker = (path: string): string | undefined => redirectedWriteBlocker(redirection, [path], "uninstall"); return { - clients: () => { - const clients = supportedClients(home, platform, environment); - redirection = appDataRedirection(clients); - return Promise.resolve( - clients.flatMap((client) => [ - ...uninstallConfigPaths(client).map((configPath) => ({ + clients: async () => { + const supported = supportedClients(home, platform, environment); + redirection = appDataRedirection(supported); + // Shared configuration alone cannot establish product applicability. + // Definitive absence leaves the shared paths unverified while owned + // rows in a readable configuration can still be removed independently; + // an unresolved probe is unknown, never absence. + const probes = new Map(); + for (const client of supported) + probes.set( + client.name, + await discoveryApplicability(client.discoveryPaths, fileSystem), + ); + const paths = sharedConfigPaths(supported); + const clients: UninstallClient[] = []; + for (const client of supported) + for (const path of paths) { + if (path.owner.client !== client) continue; + // A fallback reader constrains this file only while it may load it: + // definitive absence leaves the file to its owner. A reader that may + // load the owner's registration makes applicability one fact per + // physical file, so its installed proof applies and its unresolved + // probe keeps the file fail-closed. + const readers = path.readers.filter(({ client: reader }) => { + const probe = probes.get(reader.name); + return probe?.applicable === true || probe?.unresolved === true; + }); + const probesForPath = [path.owner, ...readers].map( + ({ client: participant }) => + probes.get(participant.name) ?? { + applicable: false, + unresolved: false, + }, + ); + // Only definitive absence may soften failures for storage REA does + // not own: a denied or failed probe leaves applicability unresolved, + // and unresolved applicability must not downgrade shared-config + // failures to retained. A denied product-local path means an unknown + // registration state, which must stop uninstall instead of reporting + // success. + const applicability: Pick = + probesForPath.some(({ applicable }) => applicable) || + probesForPath.some(({ unresolved }) => unresolved) || + !clientSharedRegistrationPaths(client).has(path.owner.configPath) + ? {} + : { applicability: "unverified" }; + clients.push({ ...client, - configPath, - })), - ...(client.zcodeFallbackPath === undefined - ? [] - : [ - { - ...client, - configPath: client.zcodeFallbackPath, - format: "zcode_fallback" as const, - }, - ]), - ]), - ); + configPath: path.owner.configPath, + ...(path.owner.fallback + ? { format: "zcode_fallback" as const } + : {}), + ...applicability, + ...(readers.length === 0 + ? {} + : { + sharedWith: readers.map(({ client: reader }) => reader.name), + }), + }); + } + return clients; }, inspectClient: (client) => configurationOperations.run(async () => { + const ownershipEstablished = + client.sharedRegistration === true && + clientOwnershipEvidencePaths(client).length > 0 && + (await hasOwnershipEvidence(client, fileSystem)); const read = await readClientConfiguration( client, fileSystem, resources, + ownershipEstablished, ); if (read.kind === "item") return read.item.status === "failed" ? read.item : undefined; @@ -301,6 +544,7 @@ export const systemUninstallHost = ( removal.value.legacyPath === undefined ) return undefined; + preflightOwnedClients.add(client); // A removal the running package would revert, or redirect into its // private storage, would leave the registration in place. const blocker = clientWriteBlocker(client); @@ -310,8 +554,12 @@ export const systemUninstallHost = ( }), removeClient: (client) => configurationOperations.run(() => - removeClient(client, fileSystem, resources, () => - clientWriteBlocker(client), + removeClient( + client, + fileSystem, + resources, + () => clientWriteBlocker(client), + preflightOwnedClients.has(client), ), ), inspectSkill: async () => { @@ -402,13 +650,71 @@ type ClientConfigurationRead = readonly parsed: ClientConfigurationDocument; }; +/** The configuration text, or the outcome to report for a failed read. A + * completed physical read is retained even when its handle fails to close: + * that exact text still drives removal, so an owned row it proves stays + * actionable instead of softening to retained. */ +const readClientConfigurationText = async ( + client: UninstallClient, + transactionPath: string, + io: { + readonly fileSystem: UninstallFileSystem; + readonly resources: ArtifactResourceScope; + readonly configurationFailure: (detail: string) => ClientConfigurationRead; + }, +): Promise => { + try { + return await io.fileSystem.readText(transactionPath); + } catch (cause: unknown) { + if (isClientConfigurationCleanupFailure(cause)) { + const cleanup = await retryClientConfigurationCleanup( + cause, + io.resources, + ); + const detail = + cleanup.primary instanceof InvalidUtf8FileError + ? "Configuration is not valid UTF-8 and was not changed; its resources could not be released. Repair it, then rerun uninstall." + : "Configuration could not be read or its resources could not be released. Check file permissions, then rerun uninstall."; + // An unreleased handle is REA's own incomplete cleanup, never foreign- + // storage uncertainty: it must stop removal outright, so later removals + // and purge cannot outrun the failure that close() reports afterwards. + if (cleanup.cleanup !== undefined) + return itemRead(item(client.name, "failed", detail)); + const completed: unknown = + cause.outcome.kind === "completed" ? cause.outcome.value : undefined; + if (typeof completed !== "string") return io.configurationFailure(detail); + return completed; + } + return isMissing(cause) + ? itemRead(item(client.name, "skipped", "Configuration does not exist.")) + : io.configurationFailure( + cause instanceof InvalidUtf8FileError + ? "Configuration is not valid UTF-8 and was not changed. Repair it, then rerun uninstall." + : "Configuration could not be read. Check file permissions, then rerun uninstall.", + ); + } +}; + const readClientConfiguration = async ( - client: SetupClient, + client: UninstallClient, fileSystem: UninstallFileSystem, resources: ArtifactResourceScope, + ownershipEstablished = false, ): Promise => { + const applicabilityUnverified = + client.applicability === "unverified" && !ownershipEstablished; + const configurationFailure = (detail: string): ClientConfigurationRead => + itemRead( + item( + client.name, + applicabilityUnverified ? "retained" : "failed", + applicabilityUnverified + ? `Product applicability could not be established. Shared configuration was retained without blocking other REA removals. ${detail}` + : detail, + ), + ); if (client.configPathError !== undefined) - return itemRead(item(client.name, "failed", client.configPathError)); + return configurationFailure(client.configPathError); if (client.format === "unsupported") return itemRead( item( @@ -419,50 +725,26 @@ const readClientConfiguration = async ( ), ); const resolved = await resolveUninstallConfigPath(client, fileSystem); - if (typeof resolved !== "string") return itemRead(resolved); + if (typeof resolved !== "string") + return resolved.status === "failed" + ? configurationFailure(resolved.detail) + : itemRead(resolved); const transactionPath = resolved; - let original: string; - try { - original = await fileSystem.readText(transactionPath); - } catch (cause: unknown) { - if (isClientConfigurationCleanupFailure(cause)) { - const cleanup = await retryClientConfigurationCleanup(cause, resources); - return itemRead( - item( - client.name, - "failed", - cleanup.primary instanceof InvalidUtf8FileError - ? "Configuration is not valid UTF-8 and was not changed; its resources could not be released. Repair it, then rerun uninstall." - : "Configuration could not be read or its resources could not be released. Check file permissions, then rerun uninstall.", - ), - ); - } - const readFailure = cause; - return itemRead( - isMissing(readFailure) - ? item(client.name, "skipped", "Configuration does not exist.") - : item( - client.name, - "failed", - readFailure instanceof InvalidUtf8FileError - ? "Configuration is not valid UTF-8 and was not changed. Repair it, then rerun uninstall." - : "Configuration could not be read. Check file permissions, then rerun uninstall.", - ), - ); - } + const original = await readClientConfigurationText(client, transactionPath, { + fileSystem, + resources, + configurationFailure, + }); + if (typeof original !== "string") return original; try { const parsed = parseClientConfiguration(original, client.format); return { kind: "configuration", transactionPath, original, parsed }; } catch (cause: unknown) { - return itemRead( - item( - client.name, - "failed", - cause instanceof ClientConfigurationShapeError || - cause instanceof ConfigurationError - ? `Configuration was not changed: ${cause.message}. Repair it, then rerun uninstall.` - : `Configuration is not valid ${client.format === "toml" || client.format === "grok" ? "TOML" : client.format === "opencode" ? "JSONC" : client.format === "hermes" || client.format === "dsh" ? "YAML" : "JSON"} and was not changed. Repair it, then rerun uninstall.`, - ), + return configurationFailure( + cause instanceof ClientConfigurationShapeError || + cause instanceof ConfigurationError + ? `Configuration was not changed: ${cause.message}. Repair it, then rerun uninstall.` + : `Configuration is not valid ${client.format === "toml" || client.format === "grok" ? "TOML" : client.format === "opencode" ? "JSONC" : client.format === "hermes" || client.format === "dsh" ? "YAML" : "JSON"} and was not changed. Repair it, then rerun uninstall.`, ); } }; @@ -484,6 +766,8 @@ type RegistrationRemoval = { type RegistrationRemovalRefusal = { readonly kind: "shadowed_unowned_member" | "foreign_legacy_fallback"; readonly detail: string; + /** The owned rows that motivated removal; refusing the edit keeps them. */ + readonly owned: RegistrationRemoval; }; const clientRegistrationRemoval = ( @@ -505,6 +789,10 @@ const clientRegistrationRemoval = ( const removeLegacy = legacyPath !== undefined && isOwnedRegistration(parsed.legacyServers[name], client); + const ownership: RegistrationRemoval = { + removeNative, + legacyPath: removeLegacy ? legacyPath : undefined, + }; if (removeNative || removeLegacy) { const shadowed = shadowedClientServers(original, parsed, name); if ( @@ -520,17 +808,16 @@ const clientRegistrationRemoval = ( return err({ kind: "shadowed_unowned_member", detail: SHADOWED_UNOWNED_MEMBER_DETAIL, + owned: ownership, }); } if (removeNative && legacyPath !== undefined && !removeLegacy) return err({ kind: "foreign_legacy_fallback", detail: UNOWNED_LEGACY_FALLBACK_DETAIL, + owned: ownership, }); - return ok({ - removeNative, - legacyPath: removeLegacy ? legacyPath : undefined, - }); + return ok(ownership); }; const SHADOWED_UNOWNED_MEMBER_DETAIL = `Configuration repeats the "${PRODUCT_IDENTITY.mcpServerKey}" member, and an earlier duplicate is not REA-owned; removing the registration the client reads would activate an earlier duplicate, so no change was made. Remove the duplicate members manually, then rerun uninstall.`; @@ -570,12 +857,18 @@ const backupClientConfiguration = async ( }; const removeClient = async ( - client: SetupClient, + client: UninstallClient, fileSystem: UninstallFileSystem, resources: ArtifactResourceScope, writeBlocker?: () => string | undefined, + preflightOwned = false, ): Promise => { - const read = await readClientConfiguration(client, fileSystem, resources); + const read = await readClientConfiguration( + client, + fileSystem, + resources, + preflightOwned, + ); if (read.kind === "item") return read.item; const { transactionPath, original, parsed } = read; const name = PRODUCT_IDENTITY.mcpServerKey; diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts index 1369198321..20b5490ce2 100644 --- a/src/application/UpdateMaintenance.ts +++ b/src/application/UpdateMaintenance.ts @@ -69,6 +69,10 @@ export const existingMaintenanceScope = async ( entryPoint, { environment, + // A registration REA wrote must be refreshed even when the product + // profile is absent, so maintenance reads ownership evidence instead of + // product applicability. Owned rows stay the only ones selected below. + evidence: "ownership", ...(resources === undefined ? {} : { resources }), }, ); @@ -91,6 +95,14 @@ export const existingMaintenanceScope = async ( continue; clients.push(registration.client); } + // Each setup action rewrites only that client's own configuration path, so + // maintenance keeps every selected client instead of deferring one view of + // a registration to another client's inspection: ZCode writes + // ~/.zcode/cli/config.json and deliberately leaves the ~/.agents/mcp.json + // fallback untouched (docs/client-configuration.md), while FreeBuff writes + // the shared file itself. Dropping FreeBuff because ZCode reads that + // fallback would leave the row both products load pinned to the old + // version. const skills = await existingSkillDestinations( home, environment, diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index 84307150b5..0b8a3311c5 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -7,6 +7,9 @@ import { analysisViewObjectDenseCapture, environmentManifestCaptureFixture, environmentStateCaptureFixture, + analysisViewJavaScriptAnalysisWithSource, + analysisViewJavaScriptEvidence, + analysisViewLayoutEvidence, } from "../../../tests/fixtures/analysisView.js"; import { describe, expect, it } from "vitest"; @@ -19,11 +22,6 @@ import { import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js"; import { functionDossierSchema } from "../../domain/hopperValues.js"; import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js"; -import { - analysisViewJavaScriptAnalysisWithSource, - analysisViewJavaScriptEvidence, - analysisViewLayoutEvidence, -} from "../../../tests/fixtures/analysisView.js"; import { inspectAnalysisView, inspectAnalysisViewValidated, diff --git a/src/application/android/AndroidAnalysisPort.ts b/src/application/android/AndroidAnalysisPort.ts index 165ea000e1..49acfe8538 100644 --- a/src/application/android/AndroidAnalysisPort.ts +++ b/src/application/android/AndroidAnalysisPort.ts @@ -1,13 +1,13 @@ import type { AnalysisExecution, ExecutionOptions, + ProviderAvailability, } from "../AnalysisProvider.js"; import type { JsonValue } from "../../domain/jsonValue.js"; import type { AndroidRequest } from "../../domain/android/androidAnalysis.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { Result } from "../../domain/result.js"; -import type { ProviderAvailability } from "../AnalysisProvider.js"; /** Provider-authored inline projection; complete raw observations stay in rawResult. */ export interface AndroidAnalysisExecution extends AnalysisExecution { diff --git a/src/application/binary/AnalysisProviderRegistry.test.ts b/src/application/binary/AnalysisProviderRegistry.test.ts index 900838dc14..904633935d 100644 --- a/src/application/binary/AnalysisProviderRegistry.test.ts +++ b/src/application/binary/AnalysisProviderRegistry.test.ts @@ -15,10 +15,12 @@ import { createAnalysisProfile } from "../../domain/analysisProfile.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; -import { AnalysisUnsupportedTargetError } from "../../domain/analysisErrorCore.js"; +import { + AnalysisUnsupportedTargetError, + AnalysisCancelledError, +} from "../../domain/analysisErrorCore.js"; import { ProviderSelectionError } from "../../domain/providerSelectionError.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; -import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; import { err, ok } from "../../domain/result.js"; const DATABASE_TARGET: BinaryTarget = { diff --git a/src/application/binary/AnalysisProviderRegistry.ts b/src/application/binary/AnalysisProviderRegistry.ts index cf1de645a0..b4dfda8ce1 100644 --- a/src/application/binary/AnalysisProviderRegistry.ts +++ b/src/application/binary/AnalysisProviderRegistry.ts @@ -39,13 +39,13 @@ import type { ProviderAvailability, ProviderIdentity, ProviderTargetSupport, + RetainedResourceScope, } from "../AnalysisProvider.js"; import { evaluateAnalysisProviderCandidate, type AnalysisProviderCandidateEvaluation, } from "./AnalysisProviderEvaluation.js"; import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; -import type { RetainedResourceScope } from "../AnalysisProvider.js"; /** Deterministic discovery truth for one configured deep provider. */ export interface AnalysisProviderCandidateStatus { diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index 908ecc82ec..52f27b883e 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -20,8 +20,8 @@ import type { ExecutionOptions, AnalysisOperationPort, ProviderIdentity, + AnalysisOperation, } from "../AnalysisProvider.js"; -import type { AnalysisOperation } from "../AnalysisProvider.js"; import { OFFICIAL_TOOL_CONTRACTS } from "../../contracts/officialToolContracts.js"; import { ENHANCED_TOOL_CONTRACTS } from "../../contracts/enhancedToolContracts.js"; import type { diff --git a/src/application/binary/BinarySessionOpen.ts b/src/application/binary/BinarySessionOpen.ts index 8b0252f7e7..15c0925fbe 100644 --- a/src/application/binary/BinarySessionOpen.ts +++ b/src/application/binary/BinarySessionOpen.ts @@ -1,5 +1,8 @@ import type { ExecutableFormatHint } from "../../domain/dosCom.js"; -import type { BinaryArchitecture } from "../../domain/binaryTargetTypes.js"; +import type { + BinaryArchitecture, + BinaryTarget, +} from "../../domain/binaryTargetTypes.js"; import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js"; import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js"; import { @@ -10,7 +13,6 @@ import { import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js"; import { parseBinaryTarget } from "../BinaryTargetResolver.js"; import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; -import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import { EvidenceIntegrityError, AnalysisSnapshotMismatchError, diff --git a/src/application/environment/environmentCompare.ts b/src/application/environment/environmentCompare.ts index bc7e29b615..fac79aea58 100644 --- a/src/application/environment/environmentCompare.ts +++ b/src/application/environment/environmentCompare.ts @@ -1,4 +1,7 @@ -import { decodedSegment } from "../../domain/analysisView/environmentStateRules.js"; +import { + decodedSegment, + same, +} from "../../domain/analysisView/environmentStateRules.js"; import type { FieldRule } from "../../domain/analysisView/environmentStateWriteRules.js"; import { fromJsonSchema } from "@modelcontextprotocol/server"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; @@ -57,7 +60,6 @@ import { type EnvironmentToolAnswer, } from "./environmentToolAnswer.js"; import type { EnvironmentToolDefinition } from "./environmentTools.js"; -import { same } from "../../domain/analysisView/environmentStateRules.js"; /** One probe capture file: its label for the report and its parsed JSON. */ export interface CompareProbeInput { diff --git a/src/application/gpu/GpuCodeObjectsService.ts b/src/application/gpu/GpuCodeObjectsService.ts index 9f39ed793e..99002fb85b 100644 --- a/src/application/gpu/GpuCodeObjectsService.ts +++ b/src/application/gpu/GpuCodeObjectsService.ts @@ -1,7 +1,9 @@ -import { inspectGpuCodeObjectsInputSchema } from "../../domain/gpu/gpuCodeObjects.js"; +import { + inspectGpuCodeObjectsInputSchema, + gpuCodeObjectsSchema, +} from "../../domain/gpu/gpuCodeObjects.js"; import type { GpuCodeObjectsPort } from "./GpuCodeObjectsPort.js"; import type { ExecutionOptions } from "../AnalysisProvider.js"; -import { gpuCodeObjectsSchema } from "../../domain/gpu/gpuCodeObjects.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { diff --git a/src/application/javascript/JavaScriptArtifactGraphContext.ts b/src/application/javascript/JavaScriptArtifactGraphContext.ts index e7f225e6a5..79e7f9861a 100644 --- a/src/application/javascript/JavaScriptArtifactGraphContext.ts +++ b/src/application/javascript/JavaScriptArtifactGraphContext.ts @@ -17,13 +17,13 @@ import type { JavaScriptArtifactAnalysis, JavaScriptArtifactAnalysisScope, } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptUnanalyzedArtifactLookup } from "../../domain/javascript/javascriptArtifactFiles.js"; -import { HTML_INLINE_SCRIPT_LOCATION_LIMIT } from "../../domain/javascript/htmlArtifactReferences.js"; import type { + JavaScriptUnanalyzedArtifactLookup, JavaScriptArtifactFile, JavaScriptArtifactFileSet, JavaScriptArtifactFileKind, } from "../../domain/javascript/javascriptArtifactFiles.js"; +import { HTML_INLINE_SCRIPT_LOCATION_LIMIT } from "../../domain/javascript/htmlArtifactReferences.js"; import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js"; import { artifactObservationEvidence, diff --git a/src/application/reverse/ReverseEngineeringService.ts b/src/application/reverse/ReverseEngineeringService.ts index 68d5cb6c02..a4adcba067 100644 --- a/src/application/reverse/ReverseEngineeringService.ts +++ b/src/application/reverse/ReverseEngineeringService.ts @@ -15,12 +15,12 @@ import { AnalysisInputError, AnalysisResourceConstraintError, AnalysisTimeoutError, + AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { AnalysisError, type AnalysisCleanupObservation, } from "../../domain/analysisErrorBase.js"; -import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; diff --git a/src/application/sqlite/SqliteDatabaseService.test.ts b/src/application/sqlite/SqliteDatabaseService.test.ts index c8792bab65..0b39c5c1ee 100644 --- a/src/application/sqlite/SqliteDatabaseService.test.ts +++ b/src/application/sqlite/SqliteDatabaseService.test.ts @@ -1,8 +1,7 @@ import { expect, it } from "vitest"; import { SqliteDatabaseService } from "./SqliteDatabaseService.js"; import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; -import { err } from "../../domain/result.js"; -import { ok } from "../../domain/result.js"; +import { err, ok } from "../../domain/result.js"; import { createAnalysisExecution } from "../AnalysisProvider.js"; import { sqliteDatabaseSchema } from "../../domain/sqlite/sqliteDatabase.js"; diff --git a/src/artifacts/ArtifactResourceScope.test.ts b/src/artifacts/ArtifactResourceScope.test.ts index 40be9c20bc..3292bdcff4 100644 --- a/src/artifacts/ArtifactResourceScope.test.ts +++ b/src/artifacts/ArtifactResourceScope.test.ts @@ -5,7 +5,10 @@ import { withArtifactFileHandle, bindArtifactFileCleanup, } from "./ArtifactFileOperation.js"; -import { ArtifactResourceRetryFailure } from "./ArtifactResourceScope.js"; +import { + ArtifactResourceRetryFailure, + ArtifactResourceScope, +} from "./ArtifactResourceScope.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { expect, it } from "vitest"; @@ -16,7 +19,6 @@ import { type ArtifactReader, } from "./ArtifactReader.js"; import { translateArtifactFailure } from "./ArtifactOperationFailure.js"; -import { ArtifactResourceScope } from "./ArtifactResourceScope.js"; const deferred = () => { let resolve: (value: T) => void = () => {}; diff --git a/src/artifacts/extraction/ArtifactExtraction.ts b/src/artifacts/extraction/ArtifactExtraction.ts index 5eb2fe7d27..ec3d2dc5a4 100644 --- a/src/artifacts/extraction/ArtifactExtraction.ts +++ b/src/artifacts/extraction/ArtifactExtraction.ts @@ -2,7 +2,11 @@ import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js"; import { resolveAsarHeaderAdmission } from "../AsarHeader.js"; import { locateGodotPck } from "../GodotPckLocation.js"; import { withArtifactRegularFile } from "../ArtifactFileOperation.js"; -import type { ArtifactCleanupAttempt } from "../ArtifactResourceScope.js"; +import type { + ArtifactCleanupAttempt, + ArtifactResourceOwner, + ArtifactResourceScope, +} from "../ArtifactResourceScope.js"; import { lstat, realpath } from "node:fs/promises"; import { dirname } from "node:path"; import { absoluteCallerPath } from "../../filesystem/callerPath.js"; @@ -44,10 +48,6 @@ import { type ArtifactOccurrence, } from "../../domain/artifactGraph.js"; import { AnalysisUnsupportedTargetError } from "../../domain/analysisErrorCore.js"; -import type { - ArtifactResourceOwner, - ArtifactResourceScope, -} from "../ArtifactResourceScope.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js"; import { scanArtifactInventoryForExtractionInScope } from "../inventory/ArtifactInventory.js"; diff --git a/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts b/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts index 3e79b0b211..74bc6b962a 100644 --- a/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts +++ b/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts @@ -7,8 +7,10 @@ import { expect, it, vi } from "vitest"; import { ArtifactReaderFailure } from "../ArtifactReader.js"; import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js"; -import { materializeArtifactInventory } from "../../../tests/fixtures/artifactInventory.js"; -import { scanCanonicalArtifactInventory } from "../../../tests/fixtures/artifactInventory.js"; +import { + materializeArtifactInventory, + scanCanonicalArtifactInventory, +} from "../../../tests/fixtures/artifactInventory.js"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; import { PyInstallerArtifactReader } from "../PyInstallerArtifactReader.js"; import { reconstructedPycHeader } from "./PythonBytecode.js"; diff --git a/src/browser/CdpBrowserProvider.ts b/src/browser/CdpBrowserProvider.ts index b0a51c72c2..f21f68c34c 100644 --- a/src/browser/CdpBrowserProvider.ts +++ b/src/browser/CdpBrowserProvider.ts @@ -1,6 +1,8 @@ import type { BrowserObservationPort } from "../application/BrowserObservationPort.js"; -import type { ProviderIdentity } from "../application/AnalysisProvider.js"; -import type { ExecutionOptions } from "../application/AnalysisProvider.js"; +import type { + ProviderIdentity, + ExecutionOptions, +} from "../application/AnalysisProvider.js"; import { sanitizeBrowserUrl, type InspectWebPageInput, diff --git a/src/browser/CdpElectronInspection.test.ts b/src/browser/CdpElectronInspection.test.ts index 41422f9c92..5155df5eaf 100644 --- a/src/browser/CdpElectronInspection.test.ts +++ b/src/browser/CdpElectronInspection.test.ts @@ -11,8 +11,7 @@ import { captureFrames, inspectCdpElectronPage, } from "./CdpElectronInspection.js"; -import type { CdpEvent } from "./CdpConnection.js"; -import type { CdpConnection } from "./CdpConnection.js"; +import type { CdpEvent, CdpConnection } from "./CdpConnection.js"; import { ingestElectronScriptEvent, type ElectronScriptDraft, diff --git a/src/browser/CdpScreenshot.ts b/src/browser/CdpScreenshot.ts index bf3612d82f..f16618a483 100644 --- a/src/browser/CdpScreenshot.ts +++ b/src/browser/CdpScreenshot.ts @@ -3,7 +3,10 @@ import type { CaptureWebScreenshotInput, WebScreenshot, } from "../domain/webScreenshot.js"; -import { createWebScreenshotArtifact } from "../domain/webScreenshot.js"; +import { + createWebScreenshotArtifact, + decodeCanonicalBase64, +} from "../domain/webScreenshot.js"; import { BrowserObservationError } from "../domain/browserObservationError.js"; import { cdpOriginScopeRefusal } from "./CdpOriginScopeError.js"; import { @@ -24,7 +27,6 @@ import { } from "./CdpCaptureDocuments.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; -import { decodeCanonicalBase64 } from "../domain/webScreenshot.js"; import { readPngHeaderDimensions } from "../domain/pngHeader.js"; interface ScreenshotContext { diff --git a/src/browser/PlaywrightBrowserScenarioProvider.test.ts b/src/browser/PlaywrightBrowserScenarioProvider.test.ts index 07b095636e..fc80920223 100644 --- a/src/browser/PlaywrightBrowserScenarioProvider.test.ts +++ b/src/browser/PlaywrightBrowserScenarioProvider.test.ts @@ -1,4 +1,7 @@ -import { failBrowserScenarioOperation } from "./PlaywrightScenarioBrowser.js"; +import { + failBrowserScenarioOperation, + PlaywrightScenarioBrowserCleanupOwner, +} from "./PlaywrightScenarioBrowser.js"; import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; // Fake-backed composition coverage; real browser verification lives in // `npm run verify:browser`. @@ -7,7 +10,6 @@ import { describe, expect, it, vi } from "vitest"; import type { BrowserScenarioSessionPort } from "./BrowserScenarioSessionPort.js"; import { PlaywrightBrowserScenarioProvider } from "./PlaywrightBrowserScenarioProvider.js"; import { sanitizeBrowserUrl } from "../domain/browserObservation.js"; -import { PlaywrightScenarioBrowserCleanupOwner } from "./PlaywrightScenarioBrowser.js"; import { browserScenarioSchema, type BrowserScenario, diff --git a/src/browser/WebBundleAnalysisWorkerServer.test.ts b/src/browser/WebBundleAnalysisWorkerServer.test.ts index 8f3db07fd0..a37d49b10b 100644 --- a/src/browser/WebBundleAnalysisWorkerServer.test.ts +++ b/src/browser/WebBundleAnalysisWorkerServer.test.ts @@ -13,8 +13,8 @@ import type { IncludedScript } from "../domain/webBundleAnalyzerState.js"; import { webBundleWorkerMessageSchema, type WebBundleWorkerMessage, + WEB_BUNDLE_WORKER_LIMITS, } from "./WebBundleAnalysisWorkerProtocol.js"; -import { WEB_BUNDLE_WORKER_LIMITS } from "./WebBundleAnalysisWorkerProtocol.js"; const makeScript = (text: string): IncludedScript => ({ script_key: `scr_${createHash("sha256").update(text).digest("hex")}`, diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts index e4d472140b..1c0f4b21b1 100644 --- a/src/browser/WebSourceMapFetcher.ts +++ b/src/browser/WebSourceMapFetcher.ts @@ -27,7 +27,10 @@ import type { WebSourceMapItem, WebSourceMaps, } from "../domain/webBundleAnalysis.js"; -import { webSourceMapsSchema } from "../domain/webBundleAnalysis.js"; +import { + webSourceMapsSchema, + DEFAULT_SOURCE_MAP_FETCH_TIMEOUT_MS, +} from "../domain/webBundleAnalysis.js"; import { createWebTextArtifact, type WebTextArtifact, @@ -45,7 +48,6 @@ import { noRuntimeOverrideSentence, selectedBrowserDeadlineMs, } from "../domain/browserDeadline.js"; -import { DEFAULT_SOURCE_MAP_FETCH_TIMEOUT_MS } from "../domain/webBundleAnalysis.js"; import { WEB_SOURCE_MAP_LIMITS } from "../domain/webSourceLocation.js"; export interface WebSourceMapRequest { diff --git a/src/browser/history/CaptureFailures.ts b/src/browser/history/CaptureFailures.ts index ddefd1a456..2418c4b1d6 100644 --- a/src/browser/history/CaptureFailures.ts +++ b/src/browser/history/CaptureFailures.ts @@ -2,8 +2,10 @@ import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js"; -import { bindArtifactFileCleanup } from "../../artifacts/ArtifactFileOperation.js"; -import { artifactFileCancellationCause } from "../../artifacts/ArtifactFileOperation.js"; +import { + bindArtifactFileCleanup, + artifactFileCancellationCause, +} from "../../artifacts/ArtifactFileOperation.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisAccessDeniedError, diff --git a/src/browser/network/PlaywrightScenarioNetwork.ts b/src/browser/network/PlaywrightScenarioNetwork.ts index c97c45afcd..e837ed4903 100644 --- a/src/browser/network/PlaywrightScenarioNetwork.ts +++ b/src/browser/network/PlaywrightScenarioNetwork.ts @@ -7,8 +7,8 @@ import type { BrowserScenarioCaptureLimits } from "../../domain/browserScenarioC import type { BrowserScenarioEventAdmission, BrowserScenarioEventBudgetReason, + BrowserScenarioEvent, } from "../../domain/browserScenarioCaptureValues.js"; -import type { BrowserScenarioEvent } from "../../domain/browserScenarioCaptureValues.js"; import type { BrowserScenarioSecrets } from "../BrowserScenarioSecrets.js"; import { captureNetworkBody, diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index dc035e31aa..3533219923 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -2,7 +2,11 @@ import { inspectDevicetreeInputSchema, devicetreeSchema, } from "../../domain/firmware/devicetree.js"; -import { devicetreeSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { + devicetreeSummaryViewResultSchema, + binaryLayoutSummaryViewResultSchema, + nativeExportsSummaryViewResultSchema, +} from "../../domain/analysisView/analysisView.js"; import { inspectPeLoadConfigInputSchema, peLoadConfigSchema, @@ -18,10 +22,6 @@ import { inspectBinaryLayoutInputSchema, binaryLayoutSchema, } from "../../domain/native/binaryLayout.js"; -import { - binaryLayoutSummaryViewResultSchema, - nativeExportsSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; import { analysisDetailOutputSchemaOf, analysisDetailSchema, diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index ec818b9ecc..cd12ee3bf2 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -1,23 +1,23 @@ import { literalFeatureMatchSchema } from "../domain/native/literalFeatureMatch.js"; import { byteEntropySchema } from "../domain/native/byteEntropy.js"; -import { byteEntropySummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; -import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js"; -import { nativeFunctionMatchingSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; -import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js"; -import { analysisDetailOutputSchemaOf } from "./analysisDetail.js"; -import { nativeFunctionSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; -import { functionDossierSchema } from "../domain/hopperValues.js"; -import { nativeInventorySummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; -import { nativeInventoryRowsSchemas } from "../domain/native/nativeInventory.js"; -import { z } from "zod"; -import { byteSearchResultSchema } from "../domain/native/bytePattern.js"; import { + byteEntropySummaryViewResultSchema, + nativeFunctionMatchingSummaryViewResultSchema, + nativeFunctionSummaryViewResultSchema, + nativeInventorySummaryViewResultSchema, artifactInventorySummaryViewResultSchema, interfaceBuilderSummaryViewResultSchema, machoSummaryViewResultSchema, managedArtifactSummaryViewResultSchema, managedMemberSummaryViewResultSchema, } from "../domain/analysisView/analysisView.js"; +import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js"; +import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js"; +import { analysisDetailOutputSchemaOf } from "./analysisDetail.js"; +import { functionDossierSchema } from "../domain/hopperValues.js"; +import { nativeInventoryRowsSchemas } from "../domain/native/nativeInventory.js"; +import { z } from "zod"; +import { byteSearchResultSchema } from "../domain/native/bytePattern.js"; import { evidenceBundleSummarySchema } from "../domain/evidenceRecordSummary.js"; import { BINARY_ARCHITECTURES, diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts index e3bca91b11..abc2e51ae1 100644 --- a/src/cutter/CutterBridgeClient.ts +++ b/src/cutter/CutterBridgeClient.ts @@ -11,7 +11,14 @@ import { assertArtifactReadActive, } from "../artifacts/ArtifactFileOperation.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; -import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisInputError, + AnalysisOutputError, + AnalysisResourceConstraintError, + resourceConstraintRecovery, +} from "../domain/analysisErrorCore.js"; import { randomUUID } from "node:crypto"; import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { createConnection } from "node:net"; @@ -19,13 +26,6 @@ import { lstat, opendir } from "node:fs/promises"; import { join } from "node:path"; import { z } from "zod"; -import { - AnalysisCapabilityUnavailableError, - AnalysisInputError, - AnalysisOutputError, - AnalysisResourceConstraintError, - resourceConstraintRecovery, -} from "../domain/analysisErrorCore.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; import type { JsonValue } from "../domain/jsonValue.js"; diff --git a/src/cutter/CutterBridgeService.ts b/src/cutter/CutterBridgeService.ts index 8df1e6f9a2..5fac87d6b4 100644 --- a/src/cutter/CutterBridgeService.ts +++ b/src/cutter/CutterBridgeService.ts @@ -16,8 +16,8 @@ import { CutterBridgeClient, type CutterBridgeExecution, type CutterDiscoveryOptions, + CUTTER_PROVIDER_IDENTITY, } from "./CutterBridgeClient.js"; -import { CUTTER_PROVIDER_IDENTITY } from "./CutterBridgeClient.js"; interface CutterBridgePort { listSessions( diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index 3d5d7df395..5dbfffc3df 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -38,8 +38,7 @@ import { jsonValueSchema, type JsonValue, } from "./jsonValue.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js"; import { freezeJsonSnapshot } from "./immutableJson.js"; import { diff --git a/src/domain/analysisView/analysisView.test.ts b/src/domain/analysisView/analysisView.test.ts index b16ca725ce..334b1dc242 100644 --- a/src/domain/analysisView/analysisView.test.ts +++ b/src/domain/analysisView/analysisView.test.ts @@ -1,4 +1,8 @@ -import { boundedViewMessages } from "./analysisViewCoverage.js"; +import { + boundedViewMessages, + completeWithinViewCoverage, + pageViewCoverage, +} from "./analysisViewCoverage.js"; import { expect, it } from "vitest"; import { functionDossierSchema } from "../hopperValues.js"; @@ -21,10 +25,6 @@ import { createJavaScriptSemanticGraph, createImmutableJavaScriptSemanticGraphSteps, } from "../javascript/javascriptSemanticGraph.js"; -import { - completeWithinViewCoverage, - pageViewCoverage, -} from "./analysisViewCoverage.js"; import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js"; import { createArtifactInspection } from "../artifactInspection.js"; import { javascriptModulePaths } from "./javascriptApplicationView.js"; diff --git a/src/domain/android/androidApplication.ts b/src/domain/android/androidApplication.ts index 8f3f4b1cf6..e3e5fff741 100644 --- a/src/domain/android/androidApplication.ts +++ b/src/domain/android/androidApplication.ts @@ -9,8 +9,7 @@ import { hasCompleteInventoryCoverage, type ParsedInventorySet, } from "../artifactInventoryEvidence.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { nativeRuntimeConvention } from "../nativeConvention.js"; import { diff --git a/src/domain/apple/appleApplication.ts b/src/domain/apple/appleApplication.ts index 15da28b6dc..8a75300e78 100644 --- a/src/domain/apple/appleApplication.ts +++ b/src/domain/apple/appleApplication.ts @@ -19,8 +19,7 @@ import { hasCompleteInventoryCoverage, type ParsedInventorySet, } from "../artifactInventoryEvidence.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { bridgeCandidateCoverageSchema, diff --git a/src/domain/bundleComparison.ts b/src/domain/bundleComparison.ts index b4b35561aa..8157964c98 100644 --- a/src/domain/bundleComparison.ts +++ b/src/domain/bundleComparison.ts @@ -9,8 +9,7 @@ import { } from "./evidenceBundle.js"; import { AnalysisInputError } from "./analysisErrorCore.js"; import type { ResidualUnknown } from "./residualUnknown.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; import { comparisonStatusSchema } from "./comparisonStatus.js"; import { localPathStringSchema } from "./localPath.js"; diff --git a/src/domain/completionLedgerGeneration.ts b/src/domain/completionLedgerGeneration.ts index 9931ce8950..2c4e98869f 100644 --- a/src/domain/completionLedgerGeneration.ts +++ b/src/domain/completionLedgerGeneration.ts @@ -12,8 +12,7 @@ import { evidenceCompletionRecordSchema, type EvidenceCompletionLedger, } from "./evidenceCompletionLedger.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; const identifierSchema = z.string().regex(IDENTIFIER_PATTERN); const identitySchema = z.strictObject({ diff --git a/src/domain/evidence.ts b/src/domain/evidence.ts index 789537f564..344ccb9bb1 100644 --- a/src/domain/evidence.ts +++ b/src/domain/evidence.ts @@ -29,8 +29,7 @@ import { immutableJsonSnapshotMember, isImmutableJsonSnapshot, } from "./immutableJson.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; import { relationshipCoverageSchema, type RelationshipCoverage, diff --git a/src/domain/functionComparisonSchemas.ts b/src/domain/functionComparisonSchemas.ts index 7c4e1dfbb5..d10c8b5318 100644 --- a/src/domain/functionComparisonSchemas.ts +++ b/src/domain/functionComparisonSchemas.ts @@ -2,8 +2,7 @@ import { z } from "zod"; import { overallStatus, summarize } from "./functionComparisonResults.js"; import { canonicalJson } from "./comparisonSemantics.js"; import { evidenceInputSchema } from "./evidenceReference.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; const comparisonStatusSchema = z.enum(["unchanged", "changed", "unknown"]); export const functionComparisonDimensionNameSchema = z.enum([ diff --git a/src/domain/harmony/harmonyApplication.ts b/src/domain/harmony/harmonyApplication.ts index 91f3562104..89e7a0e5c9 100644 --- a/src/domain/harmony/harmonyApplication.ts +++ b/src/domain/harmony/harmonyApplication.ts @@ -9,8 +9,7 @@ import { hasCompleteInventoryCoverage, type ParsedInventorySet, } from "../artifactInventoryEvidence.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { nativeRuntimeConvention } from "../nativeConvention.js"; import { diff --git a/src/domain/javascript/javascriptFeatureTraceSchemas.ts b/src/domain/javascript/javascriptFeatureTraceSchemas.ts index 2de4df99c8..f2f5a17cd0 100644 --- a/src/domain/javascript/javascriptFeatureTraceSchemas.ts +++ b/src/domain/javascript/javascriptFeatureTraceSchemas.ts @@ -2,8 +2,7 @@ import { z } from "zod"; import { evidenceSchema, providerSchema } from "../evidence.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const nodeIdSchema = prefixedDigestSchema("jag_node"); diff --git a/src/domain/javascript/javascriptRuntimeReconciliation.ts b/src/domain/javascript/javascriptRuntimeReconciliation.ts index 35b7478e7e..cb9e1a3b8e 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliation.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliation.ts @@ -10,8 +10,10 @@ import { type ParsedRuntimeCapture, } from "./javascriptRuntimeReconciliationCaptureParsing.js"; import { createJavaScriptRuntimeReconciliationResult } from "./javascriptRuntimeReconciliationResult.js"; -import type { JavaScriptRuntimeReconciliationResult } from "./javascriptRuntimeReconciliationSchemas.js"; -import type { ReconcileJavaScriptRuntimeInput } from "./javascriptRuntimeReconciliationSchemas.js"; +import type { + JavaScriptRuntimeReconciliationResult, + ReconcileJavaScriptRuntimeInput, +} from "./javascriptRuntimeReconciliationSchemas.js"; import { projectRuntimeCaptures } from "./javascriptRuntimeReconciliationRuntime.js"; import { collectStaticRuntimeCandidates } from "./javascriptRuntimeStaticCandidates.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; diff --git a/src/domain/javascript/javascriptSemanticGraphQuery.test.ts b/src/domain/javascript/javascriptSemanticGraphQuery.test.ts index 71ca2d7f18..deb23e9edc 100644 --- a/src/domain/javascript/javascriptSemanticGraphQuery.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphQuery.test.ts @@ -17,6 +17,7 @@ import { JAVASCRIPT_SEMANTIC_NODE_KINDS, JAVASCRIPT_SEMANTIC_RELATIONS, type JavaScriptSemanticGraphInput, + type JavaScriptSemanticGraphNode, } from "./javascriptSemanticGraphSchemas.js"; import { createJavaScriptSemanticGraph, @@ -30,7 +31,6 @@ import { resolveJavaScriptSemanticEvidence, type JavaScriptSemanticGraph, } from "./javascriptSemanticGraph.js"; -import type { JavaScriptSemanticGraphNode } from "./javascriptSemanticGraphSchemas.js"; import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js"; const SHA = "a".repeat(64); diff --git a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts index 5277cf2527..14fc0b8eef 100644 --- a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts @@ -11,6 +11,8 @@ import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES, JAVASCRIPT_SEMANTIC_RELATION_FAMILY, type JavaScriptSemanticGraphInput, + type JavaScriptSemanticGraphNode, + type JavaScriptSemanticGraphRelation, } from "./javascriptSemanticGraphSchemas.js"; import { createJavaScriptSemanticGraph, @@ -21,10 +23,6 @@ import { resolveJavaScriptSemanticEvidence, type JavaScriptSemanticGraph, } from "./javascriptSemanticGraph.js"; -import type { - JavaScriptSemanticGraphNode, - JavaScriptSemanticGraphRelation, -} from "./javascriptSemanticGraphSchemas.js"; import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js"; import { javaScriptSemanticQueryInputSchema } from "./javascriptSemanticQuerySchemas.js"; import type { JsonValue } from "../jsonValue.js"; diff --git a/src/domain/javascript/javascriptSemanticQuery.ts b/src/domain/javascript/javascriptSemanticQuery.ts index 4fd87aa40e..f9bc641146 100644 --- a/src/domain/javascript/javascriptSemanticQuery.ts +++ b/src/domain/javascript/javascriptSemanticQuery.ts @@ -2,8 +2,8 @@ import type { JavaScriptSemanticGraph } from "./javascriptSemanticGraph.js"; import type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation, + JavaScriptSemanticGraphUnknown, } from "./javascriptSemanticGraphSchemas.js"; -import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js"; import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js"; import { javaScriptSemanticQueryResultSchema, diff --git a/src/domain/javascript/javascriptSemanticQuerySchemas.ts b/src/domain/javascript/javascriptSemanticQuerySchemas.ts index 7c6f8172de..6002e09c41 100644 --- a/src/domain/javascript/javascriptSemanticQuerySchemas.ts +++ b/src/domain/javascript/javascriptSemanticQuerySchemas.ts @@ -8,8 +8,7 @@ import { javaScriptSemanticEvidenceContextSchema, javaScriptSemanticUnknownSchema, } from "./javascriptSemanticGraphSchemas.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node"); const literalValueSchema = z.union([ diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts index e87a91178d..5386568620 100644 --- a/src/domain/javascript/javascriptSemanticRequireOrigin.ts +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -39,7 +39,10 @@ import { import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; import { semanticIterationSources } from "./javascriptSemanticIterationSources.js"; -import { semanticCommonJsRequireOwnEnumerableAt } from "./javascriptSemanticReferenceSources.js"; +import { + semanticCommonJsRequireOwnEnumerableAt, + semanticReferenceSourcesInactive, +} from "./javascriptSemanticReferenceSources.js"; import { semanticArrayPropertyReferences, semanticObjectPropertyReferences, @@ -54,7 +57,6 @@ import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, } from "./javascriptSemanticAmbientWrites.js"; -import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js"; export interface JavaScriptRequireOrigin { readonly origin: JavaScriptModuleOrigin; diff --git a/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts b/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts index 671286fb61..9c2c36f1d0 100644 --- a/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts +++ b/src/domain/javascript/javascriptSemanticSharedChildWrites.test.ts @@ -4,9 +4,11 @@ import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; import * as t from "@babel/types"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; -import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js"; +import { + indexJavaScriptBindingsWithReceiversSteps, + analyzeJavaScriptSemantics, +} from "./javascriptSemanticAnalysis.js"; import { semanticProjectedSourcesAtRead } from "./javascriptSemanticProjectedSources.js"; -import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; import { onlyBinding } from "./javascriptSemanticAnalysis.fixture.js"; it("excludes overwritten unknown-key sources from member mutations", () => { diff --git a/src/domain/javascript/javascriptSourceAnalysis.ts b/src/domain/javascript/javascriptSourceAnalysis.ts index 77da666486..c925199756 100644 --- a/src/domain/javascript/javascriptSourceAnalysis.ts +++ b/src/domain/javascript/javascriptSourceAnalysis.ts @@ -1,11 +1,13 @@ -import type { JavaScriptSemanticProjectionCapacity } from "./javascriptSemanticFileProjection.js"; +import type { + JavaScriptSemanticProjectionCapacity, + JavaScriptSemanticFileProjection, +} from "./javascriptSemanticFileProjection.js"; import type { AnalysisError } from "../analysisErrorBase.js"; import type { AnalysisResourceConstraintError } from "../analysisErrorCore.js"; import type { Result } from "../result.js"; import type { JavaScriptArtifactFile } from "./javascriptArtifactFiles.js"; import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js"; import type { JavaScriptModuleSemanticIr } from "./javascriptModuleSemanticIr.js"; -import type { JavaScriptSemanticFileProjection } from "./javascriptSemanticFileProjection.js"; /** Portable facts collected from one inert source. */ export interface JavaScriptAnalyzedSource { diff --git a/src/domain/javascript/sourceToBundleComparisonSchemas.ts b/src/domain/javascript/sourceToBundleComparisonSchemas.ts index b8ccf7a630..d90918fead 100644 --- a/src/domain/javascript/sourceToBundleComparisonSchemas.ts +++ b/src/domain/javascript/sourceToBundleComparisonSchemas.ts @@ -4,8 +4,7 @@ import { javaScriptFileOccurrenceSchema } from "./javascriptArtifactFiles.js"; import { evidenceSchema } from "../evidence.js"; import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js"; import { historicalSourceGraphSchema } from "../referenceSourceGraph.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const nodeIdSchema = prefixedDigestSchema("jag_node"); diff --git a/src/domain/managed/managedApplicationGraph.ts b/src/domain/managed/managedApplicationGraph.ts index 36c6557906..29b5c710a4 100644 --- a/src/domain/managed/managedApplicationGraph.ts +++ b/src/domain/managed/managedApplicationGraph.ts @@ -34,8 +34,7 @@ import { addMemberRelations, relationOmissionLimitations, } from "./managedApplicationGraphRelations.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; import { readManagedInspectionEvidence } from "./managedInspectionEvidence.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; import type { EvidenceIntegrityError } from "../evidenceErrors.js"; diff --git a/src/domain/managed/managedMemberComparison.ts b/src/domain/managed/managedMemberComparison.ts index 20b40753bd..d57ac227c2 100644 --- a/src/domain/managed/managedMemberComparison.ts +++ b/src/domain/managed/managedMemberComparison.ts @@ -23,8 +23,7 @@ import { } from "./managedMemberComparisonItems.js"; import { keyMembers } from "./managedMemberComparisonMatch.js"; import { hasSignatureResolution } from "./managedMemberSignatureIdentity.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const boundedTextSchema = z.string().min(1); diff --git a/src/domain/managed/managedNativeVerificationSchemas.ts b/src/domain/managed/managedNativeVerificationSchemas.ts index 9f3859ef24..d2cdc875ea 100644 --- a/src/domain/managed/managedNativeVerificationSchemas.ts +++ b/src/domain/managed/managedNativeVerificationSchemas.ts @@ -6,8 +6,7 @@ import { cliMetadataGuidSchema, managedNativeImportSchema, } from "./managedArtifact.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const textSchema = z.string().min(1); diff --git a/src/domain/mobileApplicationGraphProjection.ts b/src/domain/mobileApplicationGraphProjection.ts index 3b25f0f4d4..b584eb6089 100644 --- a/src/domain/mobileApplicationGraphProjection.ts +++ b/src/domain/mobileApplicationGraphProjection.ts @@ -11,8 +11,7 @@ import { AnalysisInputError } from "./analysisErrorCore.js"; import { EvidenceIntegrityError } from "./evidenceErrors.js"; import { analysisInputErrorFromIssues } from "./inputIssueProjection.js"; import { err, ok, type Result } from "./result.js"; -import { digestSchema } from "./digests.js"; -import { prefixedDigestSchema } from "./digests.js"; +import { digestSchema, prefixedDigestSchema } from "./digests.js"; /** Evidence-scoped identifier schemas shared by application graph projections. */ export const projectionEvidenceIdSchema = prefixedDigestSchema("ev"); diff --git a/src/domain/reconstructionCoverage.ts b/src/domain/reconstructionCoverage.ts index 46f67833ae..54106e599f 100644 --- a/src/domain/reconstructionCoverage.ts +++ b/src/domain/reconstructionCoverage.ts @@ -20,8 +20,7 @@ import { recommendedReconstructionProbes, type ReconstructionEvaluationContext, } from "./reconstructionCoverageEvaluation.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; import { digestCanonicalValue } from "./canonicalDigest.js"; import { offsetDateTimeInstantFromEpochMilliseconds, diff --git a/src/domain/reconstructionObligationLedgerSchemas.ts b/src/domain/reconstructionObligationLedgerSchemas.ts index e7d5707a1d..ee5a8f76a1 100644 --- a/src/domain/reconstructionObligationLedgerSchemas.ts +++ b/src/domain/reconstructionObligationLedgerSchemas.ts @@ -4,8 +4,7 @@ import { STABLE_IDENTIFIER_PATTERN } from "./stringPatterns.js"; import { evidenceAuthoritySchema } from "./evidence.js"; import { evidenceBundleSchema } from "./evidenceBundle.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; import { canonicalJson } from "./comparisonSemantics.js"; import { reconstructionObligationCoverage, diff --git a/src/domain/reconstructionVerification.test.ts b/src/domain/reconstructionVerification.test.ts index 0fa4588f9b..ddd2bb25e7 100644 --- a/src/domain/reconstructionVerification.test.ts +++ b/src/domain/reconstructionVerification.test.ts @@ -4,10 +4,12 @@ import { createEvidence, type Evidence } from "./evidence.js"; import { AnalysisInputError } from "./analysisErrorCore.js"; import { createEvidenceBundle } from "./evidenceBundle.js"; import { EvidenceIntegrityError } from "./evidenceErrors.js"; -import { reconstructionVerificationResultSchema } from "./reconstructionVerificationSchemas.js"; +import { + reconstructionVerificationResultSchema, + reconstructionSpecificationSchema, +} from "./reconstructionVerificationSchemas.js"; import type { JsonValue } from "./jsonValue.js"; import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./process/processCaptureExample.js"; -import { reconstructionSpecificationSchema } from "./reconstructionVerificationSchemas.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js"; import { createResidualUnknown } from "./residualUnknown.js"; diff --git a/src/domain/reconstructionVerificationSchemas.ts b/src/domain/reconstructionVerificationSchemas.ts index 2cf3a2f9f6..1746456e3b 100644 --- a/src/domain/reconstructionVerificationSchemas.ts +++ b/src/domain/reconstructionVerificationSchemas.ts @@ -1,8 +1,7 @@ import { z } from "zod"; import { IDENTIFIER_PATTERN } from "./stringPatterns.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; import { PROCESS_COMPARISON_DIMENSIONS } from "./process/processComparison.js"; import { functionComparisonDimensionNameSchema } from "./functionComparisonSchemas.js"; diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index 0d881540f9..4a901da252 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -9,8 +9,7 @@ import { isPortableAbsoluteReferenceTarget, } from "./referenceSourcePathIdentity.js"; import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; -import { digestSchema } from "./../domain/digests.js"; -import { prefixedDigestSchema } from "./../domain/digests.js"; +import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; const boundedTextSchema = z.string().min(1); const relativePathSchema = z .string() diff --git a/src/domain/staticRuntimeCorrelation.ts b/src/domain/staticRuntimeCorrelation.ts index 37ec64ebd2..3f4b527378 100644 --- a/src/domain/staticRuntimeCorrelation.ts +++ b/src/domain/staticRuntimeCorrelation.ts @@ -16,8 +16,10 @@ import { evidenceSchema, type Evidence } from "./evidence.js"; import { hasComparisonEvidenceIdentity } from "./comparisonEvidenceIdentity.js"; import { functionComparisonDimensionNameSchema } from "./functionComparisonSchemas.js"; import type { JsonValue } from "./jsonValue.js"; -import { comparisonStatusSchema } from "./process/processComparison.js"; -import { PROCESS_COMPARISON_DIMENSIONS } from "./process/processComparison.js"; +import { + comparisonStatusSchema, + PROCESS_COMPARISON_DIMENSIONS, +} from "./process/processComparison.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts index 574e803ba0..8bce6fc32d 100644 --- a/src/evm/EvmoleInterfaceProvider.ts +++ b/src/evm/EvmoleInterfaceProvider.ts @@ -24,6 +24,7 @@ import { import { OwnedCommandFailure, ownedCommandResources, + runOwnedCommand, } from "../process/OwnedCommand.js"; import { AnalysisError, @@ -47,7 +48,6 @@ import { solidityMetadataSchema } from "../domain/evm/solidityMetadata.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { err, ok, type Result } from "../domain/result.js"; -import { runOwnedCommand } from "../process/OwnedCommand.js"; import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js"; import { diff --git a/src/flutter/FlutterBuildProvider.ts b/src/flutter/FlutterBuildProvider.ts index 6cecca4028..fe9226e43c 100644 --- a/src/flutter/FlutterBuildProvider.ts +++ b/src/flutter/FlutterBuildProvider.ts @@ -6,11 +6,9 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { setImmediate as yieldToEventLoop } from "node:timers/promises"; -import type { - ExecutionOptions, - ProviderAvailability, -} from "../application/AnalysisProvider.js"; import { + type ExecutionOptions, + type ProviderAvailability, createAnalysisExecution, type AnalysisExecution, } from "../application/AnalysisProvider.js"; @@ -26,8 +24,8 @@ import { AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, + AnalysisProtocolError, } from "../domain/analysisErrorCore.js"; -import { AnalysisProtocolError } from "../domain/analysisErrorCore.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; diff --git a/src/ghidra/GhidraDiagnostics.test.ts b/src/ghidra/GhidraDiagnostics.test.ts index a31fb46b7e..b6ce54e03e 100644 --- a/src/ghidra/GhidraDiagnostics.test.ts +++ b/src/ghidra/GhidraDiagnostics.test.ts @@ -1,7 +1,9 @@ import { ProcessOutputTokenScanner } from "../process/ProcessOutputTokenScanner.js"; -import { ghidraFailureOutput } from "./GhidraDiagnostics.js"; +import { + ghidraFailureOutput, + createGhidraDiagnostics, +} from "./GhidraDiagnostics.js"; import { expect, it } from "vitest"; -import { createGhidraDiagnostics } from "./GhidraDiagnostics.js"; it("does not reuse historical operation-scoped failure output without a current owner", () => { const diagnostics = createGhidraDiagnostics({ diff --git a/src/ghidra/GhidraDolLoadImage.test.ts b/src/ghidra/GhidraDolLoadImage.test.ts index 3a496ae6ce..8d074cfa78 100644 --- a/src/ghidra/GhidraDolLoadImage.test.ts +++ b/src/ghidra/GhidraDolLoadImage.test.ts @@ -8,9 +8,11 @@ import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { buildGamecubeDol } from "../domain/gamecubeDol.fixture.js"; import type { DolHeader } from "../domain/gamecubeDol.js"; -import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js"; +import type { + NativeLoadImageObservation, + NativeLoadImage, +} from "../domain/native/nativeLoadImage.js"; import { attestGhidraDolLoadImage } from "./GhidraDolLoadImage.js"; -import type { NativeLoadImage } from "../domain/native/nativeLoadImage.js"; import type { Result } from "../domain/result.js"; type DolTarget = Extract; diff --git a/src/ghidra/GhidraInstallation.test.ts b/src/ghidra/GhidraInstallation.test.ts index 105192ba93..20da7d3fb7 100644 --- a/src/ghidra/GhidraInstallation.test.ts +++ b/src/ghidra/GhidraInstallation.test.ts @@ -3,8 +3,7 @@ import { createInspectionResources } from "../../tests/fixtures/inspectionResour import { missingJavaProbe } from "./GhidraInstallation.fixture.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; -import { err } from "../domain/result.js"; -import { ok as javaProbeOk } from "../domain/result.js"; +import { err, ok as javaProbeOk } from "../domain/result.js"; import { parseGhidraJavaOutput, type GhidraJavaObservation, diff --git a/src/ghidra/GhidraInstallation.ts b/src/ghidra/GhidraInstallation.ts index 326ad2fadf..3b7bde3d51 100644 --- a/src/ghidra/GhidraInstallation.ts +++ b/src/ghidra/GhidraInstallation.ts @@ -15,8 +15,7 @@ import { readGhidraInstallationProperties, type GhidraInstallationFileOptions, } from "./GhidraInstallationProperties.js"; -import { constants } from "node:fs"; -import { accessSync, realpathSync, statSync } from "node:fs"; +import { constants, accessSync, realpathSync, statSync } from "node:fs"; import { delimiter as pathDelimiter, dirname, posix, win32 } from "node:path"; import type { ProviderRejectionCode } from "../contracts/providerSelection.js"; diff --git a/src/ghidra/GhidraInstallationProperties.ts b/src/ghidra/GhidraInstallationProperties.ts index 830dd7c576..c459802969 100644 --- a/src/ghidra/GhidraInstallationProperties.ts +++ b/src/ghidra/GhidraInstallationProperties.ts @@ -8,11 +8,13 @@ import { withArtifactRegularFile, } from "../artifacts/ArtifactFileOperation.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; -import { RegularFileCapacityError } from "../filesystem/readStableFileBytes.js"; +import { + RegularFileCapacityError, + readStableFileBytes, +} from "../filesystem/readStableFileBytes.js"; import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; import { fixedProducerInputAllowance } from "../domain/fixedAllowanceRemediation.js"; import { NonRegularFileReadError } from "../filesystem/RegularFile.js"; -import { readStableFileBytes } from "../filesystem/readStableFileBytes.js"; import { ghidraApplicationProperties } from "./GhidraInstallationPolicy.js"; import { ghidraFileFailure } from "./GhidraFileArtifacts.js"; diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 2214ef4997..b5fae213c1 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -4,7 +4,7 @@ import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; import { resolvedAnalysisError } from "../domain/analysisErrorBase.js"; import { createDeferred } from "../../tests/fixtures/binarySession.js"; import { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js"; -import { ok as javaProbeOk } from "../domain/result.js"; +import { ok as javaProbeOk, err, ok } from "../domain/result.js"; import type { GhidraJavaObservation } from "./GhidraJavaProbe.js"; import type { GhidraSeedReport } from "./GhidraAnalysisSeeds.js"; import type { GhidraDisabledAnalyzerReport } from "./GhidraDisabledAnalyzers.js"; @@ -43,7 +43,6 @@ import type { GhidraProviderClientFactory } from "./GhidraProviderClient.js"; import type { GhidraHeadlessLauncher } from "./GhidraLauncher.js"; import type { GhidraInstallationHost } from "./GhidraInstallation.js"; import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js"; -import { err, ok } from "../domain/result.js"; import type { Result } from "../domain/result.js"; import { GhidraSessionError } from "./GhidraSessionError.js"; import { silentLogger } from "../logger.js"; diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index f001a881db..86a874a2e0 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -32,6 +32,13 @@ import { import { ghidraMipsProfileMismatchReason, ghidraPinnedMipsLanguage, + GHIDRA_MIPS_SOFT_FLOAT_STORAGE, + ghidraMips1Limitations, + ghidraMipsSoftFloatFailure, + ghidraMipsRelocatableLimitations, + ghidraMipsSharedLimitations, + ghidraMipsSoftFloatLimitations, + isGhidraMipsSoftFloatTarget, } from "./GhidraMipsProfile.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; @@ -77,15 +84,6 @@ import { GHIDRA_PSP_LIMITATIONS, } from "./GhidraPspProfile.js"; import { ghidraPspExtensionSchema } from "./GhidraPspExtension.js"; -import { - GHIDRA_MIPS_SOFT_FLOAT_STORAGE, - ghidraMips1Limitations, - ghidraMipsSoftFloatFailure, - ghidraMipsRelocatableLimitations, - ghidraMipsSharedLimitations, - ghidraMipsSoftFloatLimitations, - isGhidraMipsSoftFloatTarget, -} from "./GhidraMipsProfile.js"; import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js"; import { ghidraPePdbLimitations } from "./GhidraPeProfile.js"; import { unverifiedGhidraBuildLimitation } from "./GhidraReleasePolicy.js"; diff --git a/src/ghidra/GhidraSnapshotCache.test.ts b/src/ghidra/GhidraSnapshotCache.test.ts index f77eade894..e7b105dbc3 100644 --- a/src/ghidra/GhidraSnapshotCache.test.ts +++ b/src/ghidra/GhidraSnapshotCache.test.ts @@ -1,4 +1,4 @@ -import { ok as javaProbeOk } from "../domain/result.js"; +import { ok as javaProbeOk, err, ok } from "../domain/result.js"; import type { GhidraJavaObservation } from "./GhidraJavaProbe.js"; import { describe, expect, it } from "vitest"; @@ -9,7 +9,6 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { functionDossierSchema } from "../domain/hopperValues.js"; import { createAnalysisProfile } from "../domain/analysisProfile.js"; import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js"; -import { err, ok } from "../domain/result.js"; import { silentLogger } from "../logger.js"; import { GhidraProvider } from "./GhidraProvider.js"; import { GhidraSessionError } from "./GhidraSessionError.js"; diff --git a/src/gpu/PtxKernels.ts b/src/gpu/PtxKernels.ts index 0d43745096..4c7d2d270b 100644 --- a/src/gpu/PtxKernels.ts +++ b/src/gpu/PtxKernels.ts @@ -1,6 +1,8 @@ -import { BinaryFormatFailure } from "../native/metadata/BinaryContainer.js"; +import { + BinaryFormatFailure, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; import type { GpuKernel } from "../domain/gpu/gpuCodeObjects.js"; -import { BinaryResourceFailure } from "../native/metadata/BinaryContainer.js"; import { GPU_REPORT_BYTES, rejectGpu } from "./GpuSource.js"; interface Token { readonly value: string; diff --git a/src/hopper/HopperMachOImage.ts b/src/hopper/HopperMachOImage.ts index e41d62953b..5dcb6d11d1 100644 --- a/src/hopper/HopperMachOImage.ts +++ b/src/hopper/HopperMachOImage.ts @@ -9,7 +9,10 @@ import { } from "../artifacts/AnalysisCleanupLease.js"; import type { RetainedResourceScope } from "../application/AnalysisProvider.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; -import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; +import type { + BinaryTarget, + HeaderArchitecture, +} from "../domain/binaryTargetTypes.js"; import { BinaryTargetError } from "../domain/configurationErrors.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; @@ -22,7 +25,6 @@ import { type FatSliceDeclaration, } from "../domain/apple/machoContainer.js"; import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; -import type { HeaderArchitecture } from "../domain/binaryTargetTypes.js"; import { HopperCancelledError, HopperStartError, diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts index 069a2741a2..eb8352ee24 100644 --- a/src/hopper/HopperProvider.ts +++ b/src/hopper/HopperProvider.ts @@ -13,6 +13,7 @@ import type { ProviderAvailability, ProviderIdentity, ProviderTargetSupport, + RetainedResourceScope, } from "../application/AnalysisProvider.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; import type { AppConfig } from "../config/types.js"; @@ -23,9 +24,10 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup. import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; import { err, ok } from "../domain/result.js"; -import type { RetainedResourceScope } from "../application/AnalysisProvider.js"; -import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; -import { AnalysisCleanupLeaseGroup } from "../artifacts/AnalysisCleanupLease.js"; +import { + AnalysisCleanupLease, + AnalysisCleanupLeaseGroup, +} from "../artifacts/AnalysisCleanupLease.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { ArtifactResourceRetryFailure, diff --git a/src/ida/IdaNativeProtocol.test.ts b/src/ida/IdaNativeProtocol.test.ts index f99091687c..83fa022e69 100644 --- a/src/ida/IdaNativeProtocol.test.ts +++ b/src/ida/IdaNativeProtocol.test.ts @@ -19,9 +19,11 @@ import { parseConfig } from "../config/parseConfig.js"; import { IdaProvider } from "./IdaProvider.js"; import { idaCapabilities } from "./IdaProviderCapabilities.js"; import { IdaSessionClient } from "./IdaSessionClient.js"; -import type { RetainedResourceScope } from "../application/AnalysisProvider.js"; +import type { + RetainedResourceScope, + AnalysisProfileResolution, +} from "../application/AnalysisProvider.js"; import { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js"; -import type { AnalysisProfileResolution } from "../application/AnalysisProvider.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; const resourceScope: RetainedResourceScope = { diff --git a/src/ida/IdaProvider.ts b/src/ida/IdaProvider.ts index e099b0ef4a..d4cec0b8ce 100644 --- a/src/ida/IdaProvider.ts +++ b/src/ida/IdaProvider.ts @@ -1,6 +1,9 @@ import { launchRegexMatchingLimitation } from "../process/RegexMatcher.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; -import { createAnalysisProfile } from "../domain/analysisProfile.js"; +import { + createAnalysisProfile, + analysisProfilesEqual, +} from "../domain/analysisProfile.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; import { isElfMetadataArchitecture, @@ -24,7 +27,6 @@ import type { AppConfig } from "../config/types.js"; import type { ConfigurationError } from "../domain/configurationErrors.js"; import { idaRegistrationDigest } from "./IdaRegistrationIdentity.js"; import { isDeepStrictEqual } from "node:util"; -import { analysisProfilesEqual } from "../domain/analysisProfile.js"; import type { Result } from "../domain/result.js"; import { readIdaConfiguration, @@ -41,10 +43,12 @@ import { } from "./IdaProviderCapabilities.js"; import { IdaSessionClient } from "./IdaSessionClient.js"; import { privateRuntimeRootCapability } from "../process/PrivateRuntimeRoot.js"; -import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { + ArtifactResourceScope, + ArtifactResourceRetryFailure, +} from "../artifacts/ArtifactResourceScope.js"; import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; import { AnalysisCleanupLeaseGroup } from "../artifacts/AnalysisCleanupLease.js"; -import { ArtifactResourceRetryFailure } from "../artifacts/ArtifactResourceScope.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import type { RetainedResourceScope } from "../application/AnalysisProvider.js"; import type { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js"; diff --git a/src/javascript/analysis/JavaScriptAnalysisWorker.ts b/src/javascript/analysis/JavaScriptAnalysisWorker.ts index ed371deb78..2efa87f177 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorker.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorker.ts @@ -1,6 +1,9 @@ import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js"; import { WorkerFrameSignal } from "../../process/WorkerFrameSignal.js"; -import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js"; +import type { + JavaScriptSemanticProjectionCapacity, + JavaScriptSemanticFileProjection, +} from "../../domain/javascript/javascriptSemanticFileProjection.js"; import { createHash, randomUUID } from "node:crypto"; import { readFile, rm, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -12,7 +15,6 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; import { estimateJavaScriptStaticApplicationProjection } from "../../domain/javascript/javascriptApplicationProjectionResources.js"; import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js"; -import type { JavaScriptSemanticFileProjection } from "../../domain/javascript/javascriptSemanticFileProjection.js"; import type { JavaScriptAnalyzedSource, JavaScriptSourceAnalysisFailure, diff --git a/src/javascript/analysis/JavaScriptAnalysisWorkerServer.ts b/src/javascript/analysis/JavaScriptAnalysisWorkerServer.ts index 0e4f03f90b..ae219d4012 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorkerServer.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorkerServer.ts @@ -1,7 +1,10 @@ import { fitJavaScriptRouteSourceFacts } from "../../domain/javascript/javascriptRouteResultBudget.js"; import { jsonValueByteLength } from "../../domain/jsonValueByteLength.js"; import type { JsonValue } from "../../domain/jsonValue.js"; -import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js"; +import type { + JavaScriptSemanticProjectionCapacity, + JavaScriptSemanticFileProjection, +} from "../../domain/javascript/javascriptSemanticFileProjection.js"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -15,7 +18,6 @@ import { javaScriptRuntime } from "../../domain/runtimeVersion.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js"; -import type { JavaScriptSemanticFileProjection } from "../../domain/javascript/javascriptSemanticFileProjection.js"; import { javaScriptTransferByteBudget, writeJavaScriptAnalysisTransfer, diff --git a/src/native/LldbCallTracer.ts b/src/native/LldbCallTracer.ts index 85f6d774a7..c9b508f421 100644 --- a/src/native/LldbCallTracer.ts +++ b/src/native/LldbCallTracer.ts @@ -14,9 +14,11 @@ import { AnalysisTimeoutError, } from "../domain/analysisErrorCore.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; -import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { + AnalysisError, + AnalysisCleanupObservation, +} from "../domain/analysisErrorBase.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; -import type { AnalysisCleanupObservation } from "../domain/analysisErrorBase.js"; import type { NativeCallPartialObservation } from "../domain/native/nativeCallPartialObservation.js"; import { nativeCallEventSchema, diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index 5a3b3efee1..1000cb97ba 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -8,6 +8,7 @@ import { resolveNativePlistSelection, unreadablePlist, type NativePlistSelection, + resolveBundleResourcePlistSelection, } from "./NativePlistSource.js"; import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; @@ -135,7 +136,6 @@ import { } from "./NativeUiHelperRuntime.js"; import { NativeUiInterruptionControl } from "./NativeUiInterruptionControl.js"; import type { ResolvedAppBundleResourceRoot } from "../application/AppBundleExecutable.js"; -import { resolveBundleResourcePlistSelection } from "./NativePlistSource.js"; import { UnsupportedBinaryPlistUidError } from "../artifacts/apple/PlistScalars.js"; type SignatureRuntimeRootFactory = () => Promise< diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts index 15ed318c43..557461efd5 100644 --- a/src/native/pwntools/PwntoolsDecoder.ts +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -17,8 +17,10 @@ import { constants } from "node:fs"; import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import { z } from "zod"; -import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; -import type { ProviderIdentity } from "../../application/AnalysisProvider.js"; +import type { + ExecutionOptions, + ProviderIdentity, +} from "../../application/AnalysisProvider.js"; import type { AnalysisCapturedOutput, AnalysisPartialObservation, diff --git a/src/pcap/PcapConfiguration.ts b/src/pcap/PcapConfiguration.ts index 9e53ba879c..9b17af045a 100644 --- a/src/pcap/PcapConfiguration.ts +++ b/src/pcap/PcapConfiguration.ts @@ -4,9 +4,11 @@ import { delimiter, isAbsolute, join } from "node:path"; import type { ProviderAvailability } from "../application/AnalysisProvider.js"; import type { ProviderRejectionCode } from "../contracts/providerSelection.js"; import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js"; -import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisArtifactChangedError, +} from "../domain/analysisErrorCore.js"; import type { JsonValue } from "../domain/jsonValue.js"; -import { AnalysisArtifactChangedError } from "../domain/analysisErrorCore.js"; export interface WiresharkExecutableIdentity { readonly bytes: number; diff --git a/src/pcap/PcapProvider.ts b/src/pcap/PcapProvider.ts index 04d9505440..586e860321 100644 --- a/src/pcap/PcapProvider.ts +++ b/src/pcap/PcapProvider.ts @@ -1,8 +1,6 @@ -import type { - ExecutionOptions, - ProviderAvailability, -} from "../application/AnalysisProvider.js"; import { + type ExecutionOptions, + type ProviderAvailability, createAnalysisExecution, type AnalysisExecution, } from "../application/AnalysisProvider.js"; diff --git a/src/process/ArtifactAnalysisResources.test.ts b/src/process/ArtifactAnalysisResources.test.ts index 4a1648577b..bca2754e3d 100644 --- a/src/process/ArtifactAnalysisResources.test.ts +++ b/src/process/ArtifactAnalysisResources.test.ts @@ -1,6 +1,8 @@ import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; -import { ArtifactResourceRetryFailure } from "../artifacts/ArtifactResourceScope.js"; -import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { + ArtifactResourceRetryFailure, + ArtifactResourceScope, +} from "../artifacts/ArtifactResourceScope.js"; import { withArtifactRegularFile } from "../artifacts/ArtifactFileOperation.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; diff --git a/src/process/capture/FilesystemCoverage.test.ts b/src/process/capture/FilesystemCoverage.test.ts index 61bb62386a..46e6ea4a3e 100644 --- a/src/process/capture/FilesystemCoverage.test.ts +++ b/src/process/capture/FilesystemCoverage.test.ts @@ -12,10 +12,12 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { processScenarioSchema } from "../../domain/process/processScenario.js"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; -import { snapshotRoots } from "./FilesystemSnapshot.js"; +import { + snapshotRoots, + ProcessCaptureSnapshotFailure, +} from "./FilesystemSnapshot.js"; import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js"; import { prepareProcessCapture } from "./ProcessCaptureLifecycle.js"; -import { ProcessCaptureSnapshotFailure } from "./FilesystemSnapshot.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; it.each(["after", "before"] as const)( diff --git a/src/process/capture/ProcessCaptureTerminalRetention.test.ts b/src/process/capture/ProcessCaptureTerminalRetention.test.ts index 5c55e057e0..d84a975a9a 100644 --- a/src/process/capture/ProcessCaptureTerminalRetention.test.ts +++ b/src/process/capture/ProcessCaptureTerminalRetention.test.ts @@ -3,13 +3,15 @@ import type { IPty } from "@lydell/node-pty"; import { processScenarioSchema } from "../../domain/process/processScenario.js"; import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js"; -import { createProcessIdentifierLookup } from "./ProcessNormalization.js"; +import { + createProcessIdentifierLookup, + createBoundedProcessTextNormalizer, +} from "./ProcessNormalization.js"; import type { TerminalFrame, ProcessSample, } from "../../domain/process/processCapture.js"; import { captureTerminalFrames } from "./ProcessCaptureLifecycle.js"; -import { createBoundedProcessTextNormalizer } from "./ProcessNormalization.js"; it("omits an expanding chunk whole and retains a later smaller normalized chunk", () => { let onData: ((data: string) => void) | undefined; diff --git a/src/server/registerElectronTools.ts b/src/server/registerElectronTools.ts index ed5641e03f..dbbd37c08a 100644 --- a/src/server/registerElectronTools.ts +++ b/src/server/registerElectronTools.ts @@ -39,8 +39,10 @@ import { } from "./toolRegistrationOptions.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; -import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { deliverEvidenceResult } from "./contractToolHandler.js"; +import { + createEvidenceToolHandler, + deliverEvidenceResult, +} from "./contractToolHandler.js"; interface ElectronToolRegistration { readonly artifactResources: ArtifactResourceScope; diff --git a/src/server/registerFridaTools.ts b/src/server/registerFridaTools.ts index 80db3d7e16..4c2503b247 100644 --- a/src/server/registerFridaTools.ts +++ b/src/server/registerFridaTools.ts @@ -4,8 +4,10 @@ import type { Logger } from "pino"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; import { FridaInstrumentationService } from "../application/frida/FridaInstrumentationService.js"; -import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js"; -import type { FridaRemoteConnection } from "../application/frida/FridaInstrumentationPort.js"; +import type { + FridaInstrumentationPort, + FridaRemoteConnection, +} from "../application/frida/FridaInstrumentationPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; diff --git a/src/wasm/WabtArtifactProvider.ts b/src/wasm/WabtArtifactProvider.ts index 56c645d6be..e555444816 100644 --- a/src/wasm/WabtArtifactProvider.ts +++ b/src/wasm/WabtArtifactProvider.ts @@ -2,12 +2,12 @@ import { randomUUID, createHash } from "node:crypto"; import { access, realpath, writeFile } from "node:fs/promises"; import { constants } from "node:fs"; import { isAbsolute, join } from "node:path"; -import type { WasmArtifactExecutionOptions } from "../application/wasm/WasmArtifactPort.js"; -import { createAnalysisExecution } from "../application/AnalysisProvider.js"; import type { + WasmArtifactExecutionOptions, WasmArtifactExecution, WasmArtifactPort, } from "../application/wasm/WasmArtifactPort.js"; +import { createAnalysisExecution } from "../application/AnalysisProvider.js"; import { completedStableArtifact, readStableArtifact, diff --git a/tests/acceptance/tools/nativeFunctionMatching.test.ts b/tests/acceptance/tools/nativeFunctionMatching.test.ts index 8c083945ff..20405c4746 100644 --- a/tests/acceptance/tools/nativeFunctionMatching.test.ts +++ b/tests/acceptance/tools/nativeFunctionMatching.test.ts @@ -9,9 +9,9 @@ import { nativeMatchingNamedInventory as nativeMatchingInventory, nativeMatchingDetails, nativeMatchingTarget, + nativeMatchingProvider, } from "../../fixtures/nativeFunctionMatching.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { nativeMatchingProvider } from "../../fixtures/nativeFunctionMatching.js"; import { procedureInfoSchema } from "../../../src/domain/native/procedureInfo.js"; import { createEvidenceBundle, diff --git a/tests/boundary/cli/versionReportCli.test.ts b/tests/boundary/cli/versionReportCli.test.ts index de4e24638d..7aa9326191 100644 --- a/tests/boundary/cli/versionReportCli.test.ts +++ b/tests/boundary/cli/versionReportCli.test.ts @@ -1,6 +1,5 @@ -import { readFile } from "node:fs/promises"; +import { readFile, mkdir } from "node:fs/promises"; import { randomUUID } from "node:crypto"; -import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts index 0347150674..2952cee12f 100644 --- a/tests/boundary/filesystem/clientLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -70,6 +70,7 @@ describe("client configuration filesystem lifecycle", () => { "droid", "zcode", "dsh", + "freebuff", "grok_bot", ]); expect( diff --git a/tests/boundary/filesystem/clientRegistrationStatus.test.ts b/tests/boundary/filesystem/clientRegistrationStatus.test.ts index 53dc032ebe..fa17c1ed1f 100644 --- a/tests/boundary/filesystem/clientRegistrationStatus.test.ts +++ b/tests/boundary/filesystem/clientRegistrationStatus.test.ts @@ -589,6 +589,78 @@ describe("Node-wrapped registration policy", () => { ); }); +describe("explicit client selection", () => { + // A regular file where the shared directory belongs fails every access + // probe (ENOTDIR on POSIX, ENOENT on Windows), which is the shape an ACL + // failure takes for automatic discovery. + const unreachableSharedParent = async (): Promise => { + const home = await createTestTempDirectory("rea-selected-unreachable-"); + await writeFile(join(home, ".agents"), "not a directory"); + return home; + }; + + it("inspects a selected client whose config location cannot be probed", async () => { + const home = await unreachableSharedParent(); + + const statuses = await readClientRegistrationStatuses( + home, + "/current/rea", + { + environment: {}, + selectedClientIds: ["freebuff"], + }, + ); + + // Before explicit selection authorised the read, every probe failed and + // the client was omitted, so doctor reported "not detected" instead. + expect(statuses).toEqual([ + expect.objectContaining({ + client: "freebuff", + config_path: join(home, ".agents", "mcp.json"), + command: [], + }), + ]); + }); + + it.skipIf(process.platform === "win32")( + "reports an unreadable selected config instead of omitting the client", + async () => { + const home = await unreachableSharedParent(); + + const statuses = await readClientRegistrationStatuses( + home, + "/current/rea", + { + environment: {}, + selectedClientIds: ["freebuff"], + }, + ); + + expect(statuses).toEqual([ + expect.objectContaining({ + client: "freebuff", + state: "invalid", + remediation: expect.stringContaining("could not be read"), + }), + ]); + }, + ); + + it("keeps automatic discovery probing when no client is selected", async () => { + const home = await unreachableSharedParent(); + + const statuses = await readClientRegistrationStatuses( + home, + "/current/rea", + { + environment: {}, + }, + ); + + expect(statuses.map(({ client }) => client)).not.toContain("freebuff"); + }); +}); + it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( "reports an inaccessible configuration target instead of omitting its registration", async () => { diff --git a/tests/boundary/filesystem/flutterBuildProvider.test.ts b/tests/boundary/filesystem/flutterBuildProvider.test.ts index 9d7176f0ca..421b27717f 100644 --- a/tests/boundary/filesystem/flutterBuildProvider.test.ts +++ b/tests/boundary/filesystem/flutterBuildProvider.test.ts @@ -11,8 +11,8 @@ import { engineFixtureImage, buildElf64Image, REFERENCE_HASH, + snapshotSection, } from "../../../src/flutter/FlutterBuild.fixture.js"; -import { snapshotSection } from "../../../src/flutter/FlutterBuild.fixture.js"; import { FlutterBuildProvider } from "../../../src/flutter/FlutterBuildProvider.js"; import { FlutterBuildAnalysisService } from "../../../src/application/flutter/FlutterBuildAnalysisService.js"; import { finishAnalysisCommand } from "../../../src/cli/finishAnalysisCommand.js"; diff --git a/tests/boundary/filesystem/freebuffClientConfiguration.test.ts b/tests/boundary/filesystem/freebuffClientConfiguration.test.ts new file mode 100644 index 0000000000..37e750beac --- /dev/null +++ b/tests/boundary/filesystem/freebuffClientConfiguration.test.ts @@ -0,0 +1,469 @@ +import { constants as fsConstants } from "node:fs"; +import { + copyFile, + lstat, + mkdir, + readFile, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js"; +import { ClientConfigurationPublication } from "../../../src/application/ClientConfigurationPublication.js"; +import { npxRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js"; +import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js"; +import { detectClients } from "../../../src/application/SetupHost.js"; +import { skillDestinations } from "../../../src/application/SetupSkill.js"; +import { + clientEvidencePaths, + clientOwnershipEvidencePaths, + supportedClients, +} from "../../../src/application/SupportedClients.js"; +import { + runUninstall, + systemUninstallHost, + type UninstallFileSystem, +} from "../../../src/application/Uninstall.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js"; + +/** + * The registration command setup writes on the host running the tests. Windows + * wraps it in `cmd.exe /d /c`, so the expected entry follows the host too. + */ +const command = npxRegistrationCommand(process.platform); + +beforeEach(clearClientLocationEnvironment); +afterEach(() => vi.unstubAllEnvs()); + +const freebuffPath = ( + home: string, + platform: NodeJS.Platform = "linux", + env: Parameters[2] = {}, +) => + supportedClients(home, platform, env).find(({ name }) => name === "freebuff"); + +describe("FreeBuff configuration paths", () => { + it("resolves the shared user agents file and the profile marker directory", () => { + expect(freebuffPath("/home/a")).toEqual({ + name: "freebuff", + displayName: "FreeBuff", + configPath: join("/home/a", ".agents", "mcp.json"), + markerPath: join("/home/a", ".config", "freebuff-desktop"), + discoveryPaths: [join("/home/a", ".config", "freebuff-desktop")], + sharedRegistration: true, + format: "json", + }); + expect(freebuffPath("/Users/a", "darwin")?.markerPath).toBe( + join("/Users/a", ".config", "freebuff-desktop"), + ); + expect(freebuffPath("C:\\Users\\a", "win32")?.markerPath).toBe( + join("C:\\Users\\a", ".config", "freebuff-desktop"), + ); + }); + + it("stays undetected when only the shared agents skill directory exists", async () => { + const home = await createTestTempDirectory("rea-freebuff-shared-"); + // REA's own skill installs create this directory for every client. + await mkdir(join(home, ".agents", "skills"), { recursive: true }); + await writeFile( + join(home, ".agents", "mcp.json"), + '{"mcpServers":{"other":{"command":"other-server"}}}', + ); + expect((await detectClients(home)).map(({ name }) => name)).not.toContain( + "freebuff", + ); + expect( + ( + await readClientRegistrationStatuses(home, undefined, { + platform: process.platform, + environment: {}, + }) + ).map(({ client }) => client), + ).not.toContain("freebuff"); + + await mkdir(join(home, ".config", "freebuff-desktop"), { + recursive: true, + }); + expect((await detectClients(home)).map(({ name }) => name)).toContain( + "freebuff", + ); + }); + + it.skipIf(process.platform === "win32")( + "ignores dangling profile links while preserving explicit and ownership reads", + async () => { + const home = await createTestTempDirectory("rea-freebuff-profile-link-"); + const profile = join(home, ".config", "freebuff-desktop"); + await mkdir(dirname(profile), { recursive: true }); + await symlink(join(home, "removed-profile"), profile, "dir"); + const statuses = (options = {}) => + readClientRegistrationStatuses(home, undefined, { + environment: {}, + ...options, + }); + expect((await detectClients(home)).map(({ name }) => name)).not.toContain( + "freebuff", + ); + expect((await statuses()).map(({ client }) => client)).not.toContain( + "freebuff", + ); + expect( + await statuses({ selectedClientIds: ["freebuff"] }), + ).toContainEqual( + expect.objectContaining({ client: "freebuff", state: "missing" }), + ); + const shared = join(home, ".agents", "mcp.json"); + await mkdir(dirname(shared), { recursive: true }); + await symlink(join(home, "removed-registration"), shared); + expect(await statuses({ evidence: "ownership" })).toContainEqual( + expect.objectContaining({ client: "freebuff", state: "missing" }), + ); + await mkdir(join(home, "removed-profile")); + expect(await statuses()).toContainEqual( + expect.objectContaining({ client: "freebuff", state: "missing" }), + ); + }, + ); + + it("keeps shared registration storage out of applicability evidence", () => { + for (const client of supportedClients("/home/a", "linux", {})) { + if (client.sharedRegistration !== true) continue; + expect(clientEvidencePaths(client)).not.toContain(client.configPath); + expect(clientOwnershipEvidencePaths(client)).toContain(client.configPath); + } + }); + + it("shares the user-scope file ZCode loads as its fallback", () => { + const clients = supportedClients("/home/a", "linux", {}); + const freebuff = clients.find(({ name }) => name === "freebuff"); + const zcode = clients.find(({ name }) => name === "zcode"); + expect(freebuff?.configPath).toBe(zcode?.zcodeFallbackPath); + }); + + it("resolves the personal skills directory FreeBuff discovers", () => { + expect(skillDestinations("/home/a", ["freebuff"])).toEqual([ + { + client: "shared", + path: join("/home/a", ".agents", "skills", "reverse-engineer-anything"), + }, + ]); + }); +}); + +describe("FreeBuff JSON registration", () => { + const freebuffClient = async () => { + const home = await createTestTempDirectory("rea-freebuff-"); + const client = freebuffPath(home, process.platform); + if (client === undefined) throw new Error("missing FreeBuff client"); + await mkdir(dirname(client.configPath), { recursive: true }); + await mkdir(client.markerPath!, { recursive: true }); + return { home, client }; + }; + + it("upserts rea into mcpServers while preserving unrelated settings", async () => { + const { home, client } = await freebuffClient(); + await writeFile( + client.configPath, + `{ + "mcpServers": { + "other": { "command": "other-server" } + }, + "agents": { "discoverSkills": true } +}`, + ); + + expect( + (await configureClientConfiguration(client, {}, command)).status, + ).toBe("configured"); + const parsed = JSON.parse(await readFile(client.configPath, "utf8")); + expect(parsed.mcpServers.other).toEqual({ command: "other-server" }); + expect(parsed.agents).toEqual({ discoverSkills: true }); + expect(parsed.mcpServers.rea).toEqual({ + command: command[0], + args: command.slice(1), + }); + + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect( + ( + await readClientRegistrationStatuses(home, undefined, { + platform: process.platform, + environment: {}, + }) + ).find(({ client }) => client === "freebuff"), + ).toMatchObject({ state: "aligned", command }); + }); + + it("preserves a shared-row enable:false while repairing FreeBuff", async () => { + // The shared file doubles as ZCode's fallback: its `enable: false` binds + // every reader, so a FreeBuff repair must not silently re-enable the row + // it does not own. Doctor reports the disabled row as stale for FreeBuff; + // the repair rewrites the registration but keeps the activation fact. + const { client } = await freebuffClient(); + await writeFile( + client.configPath, + JSON.stringify( + { + mcpServers: { + rea: { command: command[0], args: command.slice(1), enable: false }, + }, + }, + null, + 2, + ), + ); + expect(["configured", "unchanged"]).toContain( + (await configureClientConfiguration(client, {}, command)).status, + ); + const parsed = JSON.parse(await readFile(client.configPath, "utf8")); + expect(parsed.mcpServers.rea).toMatchObject({ + command: command[0], + args: command.slice(1), + enable: false, + }); + }); + + it("reports a preserved shared disable as a manual conflict, not a setup repair", async () => { + // The shared row's `enable: false` survives repair, so a stale doctor + // result must not point at an endlessly ineffective setup run: the + // remediation names the manual conflict instead. + const { home, client } = await freebuffClient(); + await writeFile( + client.configPath, + JSON.stringify( + { + mcpServers: { + rea: { command: command[0], args: command.slice(1), enable: false }, + }, + }, + null, + 2, + ), + ); + const status = ( + await readClientRegistrationStatuses(home, undefined, { + platform: process.platform, + environment: {}, + }) + ).find(({ client }) => client === "freebuff"); + expect(status).toMatchObject({ state: "stale" }); + expect(status?.remediation).toEqual( + expect.stringContaining("enable: false"), + ); + expect(status?.remediation).toEqual( + expect.stringContaining("will not change"), + ); + }); + + it("creates the mcpServers table in an existing file without servers", async () => { + const { client } = await freebuffClient(); + await writeFile( + client.configPath, + JSON.stringify({ agents: { discoverSkills: true } }, null, 2), + ); + expect( + (await configureClientConfiguration(client, {}, command)).status, + ).toBe("configured"); + const parsed = JSON.parse(await readFile(client.configPath, "utf8")); + expect(parsed.agents).toEqual({ discoverSkills: true }); + expect(parsed.mcpServers.rea).toEqual({ + command: command[0], + args: command.slice(1), + }); + }); + + it("registers into an empty settings file", async () => { + const { client } = await freebuffClient(); + await writeFile(client.configPath, ""); + expect( + (await configureClientConfiguration(client, {}, command)).status, + ).toBe("configured"); + const parsed = JSON.parse(await readFile(client.configPath, "utf8")); + expect(parsed.mcpServers.rea).toEqual({ + command: command[0], + args: command.slice(1), + }); + }); + + it("removes only the rea entry on uninstall", async () => { + const { home, client } = await freebuffClient(); + await writeFile( + client.configPath, + JSON.stringify( + { mcpServers: { other: { command: "other-server" } } }, + null, + 2, + ), + ); + await configureClientConfiguration(client, {}, command); + + expect((await systemUninstallHost(home).removeClient(client)).status).toBe( + "removed", + ); + const parsed = JSON.parse(await readFile(client.configPath, "utf8")); + expect(parsed.mcpServers.other).toEqual({ command: "other-server" }); + expect(parsed.mcpServers.rea).toBeUndefined(); + }); +}); + +describe("uninstall applicability for shared FreeBuff storage", () => { + it("retains malformed shared settings without blocking unrelated REA removal", async () => { + const home = await createTestTempDirectory("rea-freebuff-absent-"); + const client = freebuffPath(home, process.platform)!; + await mkdir(dirname(client.configPath), { recursive: true }); + await writeFile(client.configPath, "{unrelated malformed shared data"); + const codex = supportedClients(home, process.platform, {}).find( + ({ name }) => name === "codex", + )!; + await mkdir(dirname(codex.configPath), { recursive: true }); + await writeFile(codex.configPath, ""); + await configureClientConfiguration(codex, {}, command); + await mkdir(join(home, ".rea", "cache"), { recursive: true }); + await writeFile(join(home, ".rea", "cache", "owned"), "owned"); + const result = await runUninstall( + true, + systemUninstallHost(home, undefined, {}), + ); + expect(result.status).toBe("complete"); + expect(result.items.find(({ name }) => name === "freebuff")).toMatchObject({ + status: "retained", + }); + expect(result.items.find(({ name }) => name === "codex")).toMatchObject({ + status: "removed", + }); + expect(result.items.find(({ name }) => name === "cache")).toMatchObject({ + status: "removed", + }); + expect(await readFile(client.configPath, "utf8")).toBe( + "{unrelated malformed shared data", + ); + }); + + it("fails closed when a denied product profile leaves a client config unreadable", async () => { + // ZCode reads product-local storage, so an unreadable profile directory + // leaves an unknown registration state rather than an absent one. + const home = await createTestTempDirectory("rea-zcode-denied-"); + const zcode = supportedClients(home, process.platform, {}).find( + ({ name }) => name === "zcode", + )!; + await mkdir(dirname(zcode.configPath), { recursive: true }); + await writeFile(zcode.configPath, JSON.stringify({ mcpServers: {} })); + await configureClientConfiguration(zcode, {}, command); + const shared = join(home, ".agents", "mcp.json"); + await mkdir(dirname(shared), { recursive: true }); + await writeFile(shared, "{malformed shared data"); + const discovery = zcode.discoveryPaths![0]; + const denied = () => Object.assign(new Error("denied"), { code: "EACCES" }); + const fileSystem: UninstallFileSystem = { + readText: (path) => + path === zcode.configPath + ? Promise.reject(denied()) + : readFile(path, "utf8"), + copy: (source, destination) => + copyFile(source, destination, fsConstants.COPYFILE_EXCL), + publishConfiguration: (path, contents, original) => + ClientConfigurationPublication.write(path, contents, original), + stat: (path) => + path === discovery ? Promise.reject(denied()) : lstat(path), + remove: (path) => rm(path, { recursive: true }), + }; + + const result = await runUninstall( + false, + systemUninstallHost(home, fileSystem), + ); + + expect(result.status).toBe("failed"); + const zcodeItem = result.items.find(({ name }) => name === "zcode"); + expect(zcodeItem).toMatchObject({ status: "failed" }); + // The denied product-local read stays blocking, and the shared fallback + // is now inspected fail-closed too, so its malformed data is reported + // beside the denied read in the same collapsed item. + expect(zcodeItem?.detail).toContain( + "Configuration could not be read. Check file permissions, then rerun uninstall.", + ); + expect(zcodeItem?.detail).toContain( + "Configuration is not valid JSON and was not changed. Repair it, then rerun uninstall.", + ); + }); + + it("removes a recognizable owned shared row even without a FreeBuff profile", async () => { + const home = await createTestTempDirectory("rea-freebuff-owned-row-"); + const client = freebuffPath(home, process.platform)!; + await mkdir(dirname(client.configPath), { recursive: true }); + await writeFile( + client.configPath, + JSON.stringify({ mcpServers: { other: { command: "other-server" } } }), + ); + await configureClientConfiguration(client, {}, command); + const result = await runUninstall( + false, + systemUninstallHost(home, undefined, {}), + ); + expect(result.status).toBe("complete"); + expect(result.items.find(({ name }) => name === "freebuff")).toMatchObject({ + status: "removed", + }); + expect( + JSON.parse(await readFile(client.configPath, "utf8")).mcpServers, + ).toEqual({ other: { command: "other-server" } }); + }); + + it("keeps an unresolved profile probe from softening shared failures", async () => { + // A denied profile probe is not absence: when the shared file also + // cannot be read, uninstall must block instead of treating applicability + // as unproven and purging data while an installed FreeBuff may still hold + // an active REA registration. + const home = await createTestTempDirectory("rea-freebuff-denied-probe-"); + const client = freebuffPath(home, process.platform)!; + const profile = client.discoveryPaths?.[0]; + if (profile === undefined) + throw new Error("freebuff discovery path missing"); + await mkdir(profile, { recursive: true }); + await configureClientConfiguration(client, {}, command); + const state = join(home, ".rea", "state.json"); + await mkdir(dirname(state), { recursive: true }); + await writeFile(state, "{}\n"); + const denied = () => Object.assign(new Error("denied"), { code: "EACCES" }); + const fileSystem: UninstallFileSystem = { + readText: (path) => + path === client.configPath + ? Promise.reject(denied()) + : readFile(path, "utf8"), + copy: (source, destination) => + copyFile(source, destination, fsConstants.COPYFILE_EXCL), + publishConfiguration: (path, contents, original) => + ClientConfigurationPublication.write(path, contents, original), + stat: (path) => + path === profile ? Promise.reject(denied()) : lstat(path), + remove: (path) => rm(path, { recursive: true }), + }; + + const result = await runUninstall( + true, + systemUninstallHost(home, fileSystem), + ); + + expect(result.status).toBe("failed"); + expect(result.items).toContainEqual( + expect.objectContaining({ name: "freebuff", status: "failed" }), + ); + expect(result.items).toContainEqual( + expect.objectContaining({ + name: "uninstall", + status: "skipped", + detail: expect.stringContaining( + "No REA registration, skill, or data was removed", + ), + }), + ); + await expect(readFile(state, "utf8")).resolves.toBe("{}\n"); + }); +}); diff --git a/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts b/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts index 98874971af..676a8fe062 100644 --- a/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts +++ b/tests/boundary/filesystem/ghidraProfileArtifactOwnership.test.ts @@ -1,6 +1,6 @@ import * as extensionModule from "../../../src/ghidra/extensions/GhidraExtensions.js"; import { createHash } from "node:crypto"; -import { basename } from "node:path"; +import { basename, join } from "node:path"; import { GhidraClient } from "../../../src/ghidra/GhidraClient.js"; import { GhidraHeadlessLauncher } from "../../../src/ghidra/GhidraLauncher.js"; import { GhidraProvider } from "../../../src/ghidra/GhidraProvider.js"; @@ -15,7 +15,6 @@ import { writeFile, type FileHandle, } from "node:fs/promises"; -import { join } from "node:path"; import { afterEach, expect, it, vi } from "vitest"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; import { parseConfig } from "../../../src/config/parseConfig.js"; diff --git a/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts b/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts index bdd293e2ac..65dd0857ec 100644 --- a/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts +++ b/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts @@ -5,8 +5,10 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; -import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js"; -import { reconstructJavaScriptArtifact } from "../../../support/javascriptApplicationScope.js"; +import { + analyzeJavaScriptApplication, + reconstructJavaScriptArtifact, +} from "../../../support/javascriptApplicationScope.js"; import { parseEvidence } from "../../../../src/domain/evidence.js"; import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascript/javascriptApplicationGraph.js"; import { analyzeJavaScriptApplicationInputSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js"; diff --git a/tests/boundary/filesystem/javascript-applications/javascriptApplicationSignedZero.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptApplicationSignedZero.test.ts index bd4a52c675..bb070ec49d 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptApplicationSignedZero.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptApplicationSignedZero.test.ts @@ -1,4 +1,7 @@ -import { javaScriptSemanticQueryInputSchema } from "../../../../src/domain/javascript/javascriptSemanticQuerySchemas.js"; +import { + javaScriptSemanticQueryInputSchema, + javaScriptSemanticQueryResultSchema, +} from "../../../../src/domain/javascript/javascriptSemanticQuerySchemas.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -6,7 +9,6 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js"; -import { javaScriptSemanticQueryResultSchema } from "../../../../src/domain/javascript/javascriptSemanticQuerySchemas.js"; import { queryJavaScriptSemanticGraph } from "../../../../src/domain/javascript/javascriptSemanticQuery.js"; import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js"; diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts index e0c450bfc7..79256b4748 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts @@ -17,8 +17,10 @@ import { readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../../../src/domain/javascript/javascriptRuntimeReconciliation.js"; -import { reconcileJavaScriptRuntimeInputSchema } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; -import { javascriptRuntimeReconciliationResultSchema } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { + reconcileJavaScriptRuntimeInputSchema, + javascriptRuntimeReconciliationResultSchema, +} from "../../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; import { electronActiveObservationInputSchema } from "../../../../src/domain/javascript/electronActiveObservation.js"; import { createElectronActiveObservationFixtureResult } from "../../../../src/domain/javascript/electronActiveObservation.fixture.js"; diff --git a/tests/boundary/filesystem/javascriptIntegrityPolicy.test.ts b/tests/boundary/filesystem/javascriptIntegrityPolicy.test.ts index 512a36b3a3..1ca066b7ed 100644 --- a/tests/boundary/filesystem/javascriptIntegrityPolicy.test.ts +++ b/tests/boundary/filesystem/javascriptIntegrityPolicy.test.ts @@ -9,11 +9,13 @@ import { join } from "node:path"; import { createPackage, createPackageWithOptions } from "@electron/asar"; import { expect, it } from "vitest"; -import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js"; +import { + analyzeJavaScriptApplication, + readJavaScriptArtifactFiles, +} from "../../support/javascriptApplicationScope.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { createJavaScriptArtifactReader } from "../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; -import { readJavaScriptArtifactFiles } from "../../support/javascriptApplicationScope.js"; import { scanCanonicalArtifactInventory } from "../../fixtures/artifactInventory.js"; import { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/filesystem/setupClientConfigCleanupOwnership.test.ts b/tests/boundary/filesystem/setupClientConfigCleanupOwnership.test.ts index 839aa427da..068e415ceb 100644 --- a/tests/boundary/filesystem/setupClientConfigCleanupOwnership.test.ts +++ b/tests/boundary/filesystem/setupClientConfigCleanupOwnership.test.ts @@ -1,7 +1,9 @@ import { + lstat as fsLstat, mkdir, open as fsOpen, readFile, + realpath as fsRealpath, rename, writeFile, type FileHandle, @@ -29,15 +31,25 @@ import { import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js"; import { configureDetectedClients } from "../../../src/application/SetupClients.js"; import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js"; -import { systemUninstallHost } from "../../../src/application/Uninstall.js"; +import { + systemUninstallHost, + runUninstall, +} from "../../../src/application/Uninstall.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal(); - return { ...actual, open: vi.fn(actual.open) }; + return { + ...actual, + open: vi.fn(actual.open), + lstat: vi.fn(actual.lstat), + realpath: vi.fn(actual.realpath), + }; }); const openMock = vi.mocked(fsOpen); +const lstatMock = vi.mocked(fsLstat); +const realpathMock = vi.mocked(fsRealpath); let actualFs: typeof import("node:fs/promises"); beforeEach(async () => { @@ -46,26 +58,45 @@ beforeEach(async () => { "node:fs/promises", ); openMock.mockImplementation((...args) => actualFs.open(...args)); + lstatMock.mockImplementation((...args) => actualFs.lstat(...args)); + realpathMock.mockImplementation((...args) => actualFs.realpath(...args)); }); -afterEach(() => openMock.mockReset()); +afterEach(() => { + openMock.mockReset(); + lstatMock.mockReset(); + realpathMock.mockReset(); +}); -const holdReadHandle = (path: string, reason: string) => { +const holdReadHandle = ( + path: string, + reason: string, + initiallyHeld = true, + maxFailures = Number.POSITIVE_INFINITY, +) => { const closeAttempts: FileHandle[] = []; let released = false; + let held = initiallyHeld; + let remainingFailures = maxFailures; openMock.mockImplementation(async (...args) => { const handle = await actualFs.open(...args); if (String(args[0]) !== path) return handle; const nativeClose = handle.close.bind(handle); vi.spyOn(handle, "close").mockImplementation(async () => { closeAttempts.push(handle); - if (!released) throw new Error(reason); + if (held && !released && remainingFailures > 0) { + remainingFailures -= 1; + throw new Error(reason); + } await nativeClose(); }); return handle; }); return { closeAttempts, + hold: () => { + held = true; + }, release: () => { released = true; }, @@ -170,6 +201,485 @@ it("stops uninstall and retains the backup read owner when its close fails", asy } }); +it("keeps shared-ownership proof when the ownership read completes but its close fails", async () => { + // FreeBuff without a profile marker leaves its shared storage applicability- + // unverified. The completed ownership read is the proof that later read + // failures are blocking; discarding it softens every failure to retained, + // so purge runs while the active shared registration remains. + const home = await createTestTempDirectory("rea-freebuff-ownership-close-"); + const setupHost = systemSetupHost( + { platform: "linux", homeDirectory: home } as DoctorHost, + { HOME: home }, + ); + const client = (await setupHost.supportedClients()).find( + ({ name }) => name === "freebuff", + ); + if (client === undefined) throw new Error("freebuff client missing"); + await mkdir(dirname(client.configPath), { recursive: true }); + await writeFile( + client.configPath, + JSON.stringify({ mcpServers: { other: { command: "other" } } }), + ); + expect( + await configureClientConfiguration(client, {}, ["rea", "mcp"]), + ).toMatchObject({ status: "configured" }); + const state = join(home, ".rea", "state.json"); + await mkdir(dirname(state), { recursive: true }); + await writeFile(state, "{}\n"); + const { release } = holdReadHandle( + client.configPath, + "held ownership read handle", + ); + const host = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + try { + const result = await runUninstall(true, host); + expect(result.status).toBe("failed"); + expect(result.items).toContainEqual( + expect.objectContaining({ name: "freebuff", status: "failed" }), + ); + expect(result.items).toContainEqual( + expect.objectContaining({ + name: "uninstall", + status: "skipped", + detail: expect.stringContaining( + "No REA registration, skill, or data was removed", + ), + }), + ); + // Data must not be purged while the shared registration is still active. + await expect(readFile(state, "utf8")).resolves.toBe("{}\n"); + } finally { + await cleanupHeldHandle(host, release); + } +}); + +it("keeps shared-ownership proof when removal refuses a shadowed duplicate", async () => { + // The effective row is REA-owned while an earlier duplicate is foreign, so + // removal refuses the edit. That refusal must not erase ownership: without + // the positive row fact, recurring close failures soften to retained and + // purge runs while the owned row remains active. + const home = await createTestTempDirectory("rea-freebuff-shadowed-close-"); + const setupHost = systemSetupHost( + { platform: "linux", homeDirectory: home } as DoctorHost, + { HOME: home }, + ); + const client = (await setupHost.supportedClients()).find( + ({ name }) => name === "freebuff", + ); + if (client === undefined) throw new Error("freebuff client missing"); + await mkdir(dirname(client.configPath), { recursive: true }); + await writeFile( + client.configPath, + '{"mcpServers":{"rea":{"command":"foreign-server","args":[]},"rea":{"command":"rea","args":["mcp"]}}}', + ); + const state = join(home, ".rea", "state.json"); + await mkdir(dirname(state), { recursive: true }); + await writeFile(state, "{}\n"); + const { release } = holdReadHandle( + client.configPath, + "held ownership read handle", + ); + const host = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + try { + const result = await runUninstall(true, host); + expect(result.status).toBe("failed"); + expect(result.items).toContainEqual( + expect.objectContaining({ name: "freebuff", status: "failed" }), + ); + expect(result.items).toContainEqual( + expect.objectContaining({ + name: "uninstall", + status: "skipped", + detail: expect.stringContaining( + "No REA registration, skill, or data was removed", + ), + }), + ); + await expect(readFile(state, "utf8")).resolves.toBe("{}\n"); + } finally { + await cleanupHeldHandle(host, release); + } +}); + +it("stops removal when a completed reread leaves its handle unreleased", async () => { + // Greptile P1: preflight and reread both complete with a foreign-only row, + // but the reread handle cannot be closed even by the cleanup retry. REA + // still owns that open handle, so removal must stop before purge: serving + // the completed text would let removeAll delete managed data while the + // incomplete cleanup is only reported later, by close(). + const home = await createTestTempDirectory("rea-freebuff-reread-leak-"); + const setupHost = systemSetupHost( + { platform: "linux", homeDirectory: home } as DoctorHost, + { HOME: home }, + ); + const client = (await setupHost.supportedClients()).find( + ({ name }) => name === "freebuff", + ); + if (client === undefined) throw new Error("freebuff client missing"); + await mkdir(dirname(client.configPath), { recursive: true }); + await writeFile( + client.configPath, + '{"mcpServers":{"rea":{"command":"foreign-server","args":[]}}}', + ); + const state = join(home, ".rea", "state.json"); + await mkdir(dirname(state), { recursive: true }); + await writeFile(state, "{}\n"); + const { hold, release } = holdReadHandle( + client.configPath, + "held removal reread handle", + false, + ); + const base = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + const host = { + ...base, + inspectSkill: async () => { + hold(); + return base.inspectSkill?.(); + }, + }; + try { + const result = await runUninstall(true, host); + expect(result.status).toBe("failed"); + expect(result.items).toContainEqual( + expect.objectContaining({ + name: "freebuff", + status: "failed", + detail: expect.stringContaining("its resources could not be released"), + }), + ); + expect(result.items).toContainEqual( + expect.objectContaining({ + name: "uninstall", + status: "skipped", + detail: expect.stringContaining("Uninstall stopped at this failure"), + }), + ); + // Managed data must survive an unreleased read handle. + await expect(readFile(state, "utf8")).resolves.toBe("{}\n"); + } finally { + await cleanupHeldHandle(host, release); + } +}); + +it("keeps the shared registration fail-closed when ZCode's probe is unresolved", async () => { + // A denied profile probe leaves ZCode's applicability unresolved, never + // absent: the shared file stays fail-closed so its row is inspected and + // removed instead of leaving an installed ZCode wired to purged data. + // Definitive absence still leaves the file to FreeBuff, ZCode's owner. + const home = await createTestTempDirectory("rea-zcode-fallback-unresolved-"); + const fallback = join(home, ".agents", "mcp.json"); + const profile = join(home, ".zcode", "cli"); + lstatMock.mockImplementation((path) => + String(path) === profile + ? Promise.reject( + Object.assign(new Error("EACCES: permission denied, lstat"), { + code: "EACCES", + }), + ) + : actualFs.lstat(path), + ); + const unresolved = systemUninstallHost( + home, + undefined, + { HOME: home }, + "linux", + ); + try { + const rows = (await unresolved.clients()).filter( + (candidate) => candidate.configPath === fallback, + ); + // One physical file, one owner: FreeBuff edits the shared file once and + // ZCode reports that same outcome instead of racing it to "absent". + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ name: "freebuff", sharedWith: ["zcode"] }); + // Fail-closed: the unresolved reader carries no unverified softening, so a + // failed read blocks removal instead of retaining around the purge. + expect(rows[0]?.applicability).toBeUndefined(); + } finally { + await unresolved.close?.(); + } + lstatMock.mockImplementation((...args) => actualFs.lstat(...args)); + const absent = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + try { + const rows = (await absent.clients()).filter( + (candidate) => candidate.configPath === fallback, + ); + expect(rows).toHaveLength(1); + // Definitive absence keeps ZCode out of another product's primary storage. + expect(rows[0]?.sharedWith).toBeUndefined(); + } finally { + await absent.close?.(); + } +}); + +it("removes a shared registration once and reports that outcome to both clients", async () => { + // ZCode and FreeBuff both load ~/.agents/mcp.json. Queuing it per client + // removed the row as ZCode and then reported FreeBuff as "absent", losing + // the outcome for the client that owns the file, and it read and edited one + // shared resource twice. + const home = await createTestTempDirectory("rea-shared-removal-once-"); + const shared = join(home, ".agents", "mcp.json"); + await mkdir(join(home, ".zcode", "cli"), { recursive: true }); + await mkdir(join(home, ".config", "freebuff-desktop"), { recursive: true }); + expect( + await configureClientConfiguration( + { name: "freebuff", format: "json", configPath: shared }, + {}, + ["rea", "mcp"], + ), + ).toMatchObject({ status: "configured" }); + const host = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + try { + const result = await runUninstall(false, host); + expect(result.status).toBe("complete"); + expect(result.items).toContainEqual( + expect.objectContaining({ + name: "freebuff", + status: "removed", + configuration_path: shared, + }), + ); + // The fallback reader reports the owner's removal, never "absent". + const zcode = result.items.find(({ name }) => name === "zcode"); + expect(zcode?.actions).toContainEqual( + expect.objectContaining({ + status: "removed", + configuration_path: shared, + }), + ); + expect(zcode?.actions).not.toContainEqual( + expect.objectContaining({ + status: "skipped", + configuration_path: shared, + }), + ); + expect(JSON.parse(await readFile(shared, "utf8"))).toEqual({ + mcpServers: {}, + }); + } finally { + await host.close?.(); + } +}); + +it("keeps one row per client when OpenCode is pointed at FreeBuff's file", async () => { + // OPENCODE_CONFIG can make OpenCode and FreeBuff edit one file with different + // tables (`mcp.rea` and `mcpServers.rea`). Sharing one decision by filename + // would remove only OpenCode's row and report it for FreeBuff too, leaving + // FreeBuff registered while uninstall reports complete. + const home = await createTestTempDirectory("rea-shared-dialects-"); + const shared = join(home, ".agents", "mcp.json"); + await mkdir(dirname(shared), { recursive: true }); + await mkdir(join(home, ".config", "freebuff-desktop"), { recursive: true }); + expect( + await configureClientConfiguration( + { name: "freebuff", format: "json", configPath: shared }, + {}, + ["rea", "mcp"], + ), + ).toMatchObject({ status: "configured" }); + const host = systemUninstallHost( + home, + undefined, + { HOME: home, OPENCODE_CONFIG: shared }, + "linux", + ); + try { + const rows = (await host.clients()).filter( + (candidate) => candidate.configPath === shared, + ); + expect(rows.map(({ name }) => name).sort()).toEqual([ + "freebuff", + "opencode", + ]); + expect(await runUninstall(false, host)).toMatchObject({ + status: "complete", + }); + expect(JSON.parse(await readFile(shared, "utf8"))).toEqual({ + mcpServers: {}, + }); + } finally { + await host.close?.(); + } +}); + +it.each([true, false])( + "projects only the shared table's outcome to ZCode (shared row owned: %s)", + async (sharedOwned) => { + const home = await createTestTempDirectory("rea-shared-table-outcome-"); + const shared = join(home, ".agents", "mcp.json"); + const zcodePath = join(home, ".zcode", "cli", "config.json"); + await mkdir(dirname(shared), { recursive: true }); + await mkdir(dirname(zcodePath), { recursive: true }); + await writeFile(zcodePath, '{"mcp":{"servers":{}}}'); + await writeFile( + shared, + JSON.stringify({ + mcp: { + rea: { + type: "local", + command: [sharedOwned ? "foreign-server" : "rea", "mcp"], + }, + }, + mcpServers: { + rea: { + command: sharedOwned ? "rea" : "foreign-server", + args: ["mcp"], + }, + }, + }), + ); + const host = systemUninstallHost( + home, + undefined, + { HOME: home, OPENCODE_CONFIG: shared }, + "linux", + ); + try { + const result = await runUninstall(false, host); + expect(result.status).toBe("complete"); + const zcode = result.items.find(({ name }) => name === "zcode"); + expect(zcode?.status).toBe(sharedOwned ? "removed" : "retained"); + expect( + zcode?.actions?.filter( + ({ configuration_path }) => configuration_path === shared, + ), + ).toEqual([ + expect.objectContaining({ + status: sharedOwned ? "removed" : "retained", + }), + ]); + const remaining = JSON.parse(await readFile(shared, "utf8")); + expect(remaining.mcp).toEqual( + sharedOwned + ? { rea: { type: "local", command: ["foreign-server", "mcp"] } } + : {}, + ); + expect(remaining.mcpServers).toEqual( + sharedOwned + ? {} + : { rea: { command: "foreign-server", args: ["mcp"] } }, + ); + } finally { + await host.close?.(); + } + }, +); + +it("does not treat a dangling FreeBuff profile link as installed", async () => { + // Setup and doctor follow ~/.config/freebuff-desktop and reject a link whose + // target is gone. Discovery must agree: counting the leftover link as an + // installed product turns an unrelated malformed shared file into a blocking + // failure that stops every other REA removal. + const home = await createTestTempDirectory("rea-freebuff-dangling-profile-"); + const shared = join(home, ".agents", "mcp.json"); + const profile = join(home, ".config", "freebuff-desktop"); + const cursor = join(home, ".cursor", "mcp.json"); + await mkdir(dirname(shared), { recursive: true }); + await writeFile(shared, "{ not json"); + await mkdir(dirname(cursor), { recursive: true }); + await writeFile(cursor, '{"keep":"before setup"}\n'); + expect( + await configureClientConfiguration( + { name: "cursor", format: "json", configPath: cursor }, + {}, + ["rea", "mcp"], + ), + ).toMatchObject({ status: "configured" }); + lstatMock.mockImplementation((path) => + String(path) === profile + ? Promise.resolve({ + isFile: () => false, + isDirectory: () => false, + isSymbolicLink: () => true, + } as unknown as Awaited>) + : actualFs.lstat(path), + ); + realpathMock.mockImplementation((path) => + String(path) === profile + ? Promise.reject( + Object.assign( + new Error("ENOENT: no such file or directory, realpath"), + { code: "ENOENT" }, + ), + ) + : actualFs.realpath(path), + ); + const host = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + try { + const result = await runUninstall(false, host); + expect(result.status).toBe("complete"); + // The unrelated registration is removed instead of blocked. + expect(result.items).toContainEqual( + expect.objectContaining({ name: "cursor", status: "removed" }), + ); + expect(JSON.parse(await readFile(cursor, "utf8"))).not.toHaveProperty( + "mcpServers.rea", + ); + expect(result.items).toContainEqual( + expect.objectContaining({ name: "freebuff", status: "retained" }), + ); + } finally { + await host.close?.(); + } +}); + +it("serves a completed removal reread when its close retry releases the handle", async () => { + // Preflight saw no owned row; another writer adds one before removal. The + // reread's first close fails but its cleanup retry releases the handle: + // the completed text is the ownership evidence that drives removal of the + // concurrently added row instead of softening to retained and leaving it + // active while data is purged. + const home = await createTestTempDirectory("rea-freebuff-reread-close-"); + const setupHost = systemSetupHost( + { platform: "linux", homeDirectory: home } as DoctorHost, + { HOME: home }, + ); + const client = (await setupHost.supportedClients()).find( + ({ name }) => name === "freebuff", + ); + if (client === undefined) throw new Error("freebuff client missing"); + await mkdir(dirname(client.configPath), { recursive: true }); + await writeFile( + client.configPath, + '{"mcpServers":{"rea":{"command":"foreign-server","args":[]}}}', + ); + const cacheOwned = join(home, ".rea", "cache", "owned"); + await mkdir(dirname(cacheOwned), { recursive: true }); + await writeFile(cacheOwned, "owned\n"); + const { hold, release } = holdReadHandle( + client.configPath, + "held removal reread handle", + false, + 1, + ); + const base = systemUninstallHost(home, undefined, { HOME: home }, "linux"); + const host = { + ...base, + inspectSkill: async () => { + // Another writer registers REA between preflight and removal. + await writeFile( + client.configPath, + '{"mcpServers":{"rea":{"command":"rea","args":["mcp"]}}}', + ); + hold(); + return base.inspectSkill?.(); + }, + }; + try { + const result = await runUninstall(true, host); + expect(result.status).toBe("complete"); + expect(result.items).toContainEqual( + expect.objectContaining({ name: "freebuff", status: "removed" }), + ); + expect(await readFile(client.configPath, "utf8")).not.toContain("rea"); + await expect(readFile(cacheOwned)).rejects.toMatchObject({ + code: "ENOENT", + }); + } finally { + await cleanupHeldHandle(host, release); + } +}); + describe("ZCode fallback cleanup ownership", () => { it.each(["preflight", "configure"])( "retains a failed ZCode fallback close during %s until host cleanup", diff --git a/tests/boundary/filesystem/zcodeClientConfiguration.test.ts b/tests/boundary/filesystem/zcodeClientConfiguration.test.ts index 9e1b6026c3..39e2c82c91 100644 --- a/tests/boundary/filesystem/zcodeClientConfiguration.test.ts +++ b/tests/boundary/filesystem/zcodeClientConfiguration.test.ts @@ -42,6 +42,7 @@ describe("ZCode configuration paths", () => { displayName: "ZCode", configPath: join("/home/a", ".zcode", "cli", "config.json"), markerPath: join("/home/a", ".zcode", "cli"), + discoveryPaths: [join("/home/a", ".zcode", "cli")], format: "zcode", zcodeFallbackPath: join("/home/a", ".agents", "mcp.json"), }); @@ -270,8 +271,11 @@ describe("ZCode fallback uninstall", () => { }; it.each(["comment", "trailing-comma", "bom"])( - "fails closed on strict fallback syntax: %s", + "edits the owner's dialect instead of ZCode's strict fallback syntax: %s", async (mode) => { + // ~/.agents/mcp.json is FreeBuff's own configuration, so FreeBuff's + // dialect decides whether it is editable. ZCode's stricter fallback view + // must not block a removal the file's owner can perform. const { home } = await zcodeClient(); const valid = await writeFallback(home, { rea: owned, @@ -285,13 +289,22 @@ describe("ZCode fallback uninstall", () => { : valid.replace(/\}\s*$/u, ",}"); await writeFile(fallbackPath(home), original); const result = await runUninstall(false, systemUninstallHost(home)); + expect(result.status).toBe("complete"); + // The shared file is removed once and both of its clients report that. expect(result.items).toContainEqual( - expect.objectContaining({ name: "zcode", status: "failed" }), + expect.objectContaining({ name: "freebuff", status: "removed" }), + ); + expect( + result.items + .find(({ name }) => name === "zcode") + ?.actions?.map(({ status }) => status), + ).toContain("removed"); + const edited = await readFile(fallbackPath(home), "utf8"); + expect(edited).not.toContain('"rea"'); + expect(edited).toContain("other-server"); + expect(await readFile(`${fallbackPath(home)}.rea.backup`, "utf8")).toBe( + original, ); - expect(await readFile(fallbackPath(home), "utf8")).toBe(original); - await expect( - readFile(`${fallbackPath(home)}.rea.backup`), - ).rejects.toMatchObject({ code: "ENOENT" }); }, ); @@ -450,7 +463,13 @@ describe("ZCode disabled registrations", () => { environment: {}, }) ).find(({ client }) => client === "zcode"); - expect(await statusFor()).toMatchObject({ state: "stale" }); + // Native ZCode storage has a single reader, so setup drops the flag + // and is the working repair; doctor must keep recommending it. + expect(await statusFor()).toMatchObject({ + state: "stale", + remediation: expect.stringContaining("rea setup"), + }); + expect((await statusFor())?.remediation).not.toContain("enable: false"); expect( (await configureClientConfiguration(client, {}, command)).status, @@ -528,6 +547,9 @@ describe("ZCode disabled registrations", () => { config_path: fallback, command, state: "stale", + // The fallback view of shared storage is preserved by setup, so its + // disable is the manual conflict rather than a setup repair. + remediation: expect.stringContaining("enable: false"), }); }); diff --git a/tests/boundary/mcp/devicetreeMcp.test.ts b/tests/boundary/mcp/devicetreeMcp.test.ts index 506107164a..71806dd0e5 100644 --- a/tests/boundary/mcp/devicetreeMcp.test.ts +++ b/tests/boundary/mcp/devicetreeMcp.test.ts @@ -1,7 +1,7 @@ import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { unlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { expect } from "vitest"; +import { expect, onTestFinished } from "vitest"; import { z } from "zod"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; @@ -12,7 +12,6 @@ import { DevicetreeProvider } from "../../../src/firmware/devicetree/DevicetreeP import { DevicetreeService } from "../../../src/application/binaryDiagnostics/DevicetreeService.js"; import { silentLogger } from "../../../src/logger.js"; import type { Evidence } from "../../../src/domain/evidence.js"; -import { onTestFinished } from "vitest"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index 2814c0cf06..c8e019d8e7 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -1,5 +1,5 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; -import { ok as resultOk } from "../../../src/domain/result.js"; +import { ok as resultOk, err } from "../../../src/domain/result.js"; import { rm, symlink, writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -31,7 +31,6 @@ import { toolAvailabilityInventory } from "../../../src/contracts/toolOutputSche import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js"; import { emptyElectronPageCoverage } from "../../../src/domain/javascript/electronPageInspection.fixture.js"; import type { ElectronPageInspection } from "../../../src/domain/javascript/electronObservation.js"; -import { err } from "../../../src/domain/result.js"; const browsers: FakeCdpBrowser[] = []; const resources: Array<{ close(): Promise }> = []; diff --git a/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts b/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts index 2a954569f5..68313a7be4 100644 --- a/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts +++ b/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts @@ -14,8 +14,7 @@ import type {} from "../../../src/domain/jsonValue.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; -import { ok as resultOk } from "../../../src/domain/result.js"; -import { err } from "../../../src/domain/result.js"; +import { ok as resultOk, err } from "../../../src/domain/result.js"; import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js"; import { nativeDispatchMetadataResultSchema } from "../../../src/domain/native/objcSwiftMetadata.js"; diff --git a/tests/boundary/mcp/erlangBeamMcp.test.ts b/tests/boundary/mcp/erlangBeamMcp.test.ts index df0702aa27..8b45ed2c78 100644 --- a/tests/boundary/mcp/erlangBeamMcp.test.ts +++ b/tests/boundary/mcp/erlangBeamMcp.test.ts @@ -4,7 +4,10 @@ import { registerErlangTools } from "../../../src/server/registerErlangTools.js" import { createToolResultDelivery } from "../../../src/server/toolResult.js"; import { BeamService } from "../../../src/application/erlang/BeamService.js"; import { BeamProvider } from "../../../src/erlang/BeamProvider.js"; -import { erlangBeamSchema } from "../../../src/domain/erlang/beam.js"; +import { + erlangBeamSchema, + MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS, +} from "../../../src/domain/erlang/beam.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; @@ -19,8 +22,6 @@ import { import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js"; -import { MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS } from "../../../src/domain/erlang/beam.js"; - const completeBeamEvidenceSchema = toolContract( "inspect_erlang_beam", ).outputSchema.safeExtend({ normalized_result: erlangBeamSchema }); diff --git a/tests/boundary/mcp/ghidraMcpHarness.ts b/tests/boundary/mcp/ghidraMcpHarness.ts index 13e2a272fa..fbe8dd4e53 100644 --- a/tests/boundary/mcp/ghidraMcpHarness.ts +++ b/tests/boundary/mcp/ghidraMcpHarness.ts @@ -1,4 +1,4 @@ -import { ok as javaProbeOk } from "../../../src/domain/result.js"; +import { ok as javaProbeOk, ok } from "../../../src/domain/result.js"; import type { GhidraJavaObservation } from "../../../src/ghidra/GhidraJavaProbe.js"; import { expect } from "vitest"; import { parseEvidence } from "../../../src/domain/evidence.js"; @@ -8,7 +8,6 @@ import type { BinarySession } from "../../../src/application/binary/BinarySessio import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js"; import { parseConfig } from "../../../src/config/parseConfig.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; -import { ok } from "../../../src/domain/result.js"; import type { GhidraInstallationHost } from "../../../src/ghidra/GhidraInstallation.js"; import type { GhidraOperation } from "../../../src/ghidra/GhidraClient.js"; import { GhidraProvider } from "../../../src/ghidra/GhidraProvider.js"; diff --git a/tests/boundary/mcp/ghidraReopen.test.ts b/tests/boundary/mcp/ghidraReopen.test.ts index ac2dbcce7c..fc2bdc7232 100644 --- a/tests/boundary/mcp/ghidraReopen.test.ts +++ b/tests/boundary/mcp/ghidraReopen.test.ts @@ -1,4 +1,4 @@ -import { ok as javaProbeOk } from "../../../src/domain/result.js"; +import { ok as javaProbeOk, ok } from "../../../src/domain/result.js"; import type { GhidraJavaObservation } from "../../../src/ghidra/GhidraJavaProbe.js"; import { spawn, type ChildProcess } from "node:child_process"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; @@ -13,7 +13,6 @@ import { composeBinarySession } from "../../../src/application/binary/BinarySess import { LazyAnalysisProvider } from "../../../src/application/binary/LazyAnalysisProvider.js"; import { parseConfig } from "../../../src/config/parseConfig.js"; import { elf } from "../../../src/domain/binaryTarget.fixture.js"; -import { ok } from "../../../src/domain/result.js"; import { GhidraClient } from "../../../src/ghidra/GhidraClient.js"; import type { GhidraInstallationHost } from "../../../src/ghidra/GhidraInstallation.js"; import type { diff --git a/tests/boundary/mcp/largeToolResult.test.ts b/tests/boundary/mcp/largeToolResult.test.ts index 6acaf5aad3..1c886e5090 100644 --- a/tests/boundary/mcp/largeToolResult.test.ts +++ b/tests/boundary/mcp/largeToolResult.test.ts @@ -28,9 +28,9 @@ import { MINIMUM_RESPONSE_BYTES } from "../../../src/config/mcpResponseBudget.js import { MCP_RESULT_STRING_LIMIT, NODE_MAX_STRING_LENGTH, + encodeToolResult, } from "../../../src/server/toolResultEncoding.js"; import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; -import { encodeToolResult } from "../../../src/server/toolResultEncoding.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; const delivery = createToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE); diff --git a/tests/boundary/mcp/nativeUiAvailability.test.ts b/tests/boundary/mcp/nativeUiAvailability.test.ts index 113e54894c..9f30ae713c 100644 --- a/tests/boundary/mcp/nativeUiAvailability.test.ts +++ b/tests/boundary/mcp/nativeUiAvailability.test.ts @@ -11,6 +11,7 @@ import type { AnalysisClient, AnalysisProvider, CapabilityDescriptor, + AnalysisOperation, } from "../../../src/application/AnalysisProvider.js"; import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js"; @@ -27,7 +28,6 @@ import { nativeInvestigationTraceSchema } from "../../../src/domain/native/nativ import { thinMach } from "../../../src/domain/binaryTarget.fixture.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; import { err, ok, type Result } from "../../../src/domain/result.js"; -import type { AnalysisOperation } from "../../../src/application/AnalysisProvider.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; diff --git a/tests/boundary/mcp/promptMcp.test.ts b/tests/boundary/mcp/promptMcp.test.ts index ef4469dcd3..cbc1e527c4 100644 --- a/tests/boundary/mcp/promptMcp.test.ts +++ b/tests/boundary/mcp/promptMcp.test.ts @@ -1,4 +1,4 @@ -import { ok as resultOk } from "../../../src/domain/result.js"; +import { ok as resultOk, err, ok } from "../../../src/domain/result.js"; import { readFile, rm, writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; @@ -31,7 +31,6 @@ import { AnalysisProviderRegistry } from "../../../src/application/binary/Analys import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js"; -import { err, ok } from "../../../src/domain/result.js"; import { createCacheProvider, createTestBinarySession, diff --git a/tests/boundary/process/processPreparation.test.ts b/tests/boundary/process/processPreparation.test.ts index 293f91d2b6..8ce62ab36f 100644 --- a/tests/boundary/process/processPreparation.test.ts +++ b/tests/boundary/process/processPreparation.test.ts @@ -3,12 +3,14 @@ import { expect, it, vi } from "vitest"; import { AnalysisError } from "../../../src/domain/analysisErrorBase.js"; import { AnalysisAccessDeniedError } from "../../../src/domain/analysisErrorCore.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; -import { ProcessCaptureResourceScope } from "../../../src/process/capture/ProcessCaptureLifecycle.js"; +import { + ProcessCaptureResourceScope, + prepareProcessCapture, +} from "../../../src/process/capture/ProcessCaptureLifecycle.js"; import { ProcessCaptureSnapshotFailure, snapshotRoots, } from "../../../src/process/capture/FilesystemSnapshot.js"; -import { prepareProcessCapture } from "../../../src/process/capture/ProcessCaptureLifecycle.js"; import { processScenarioSchema } from "../../../src/domain/process/processScenario.js"; import { emptyProcessCapture } from "../../../src/domain/process/processCapture.fixture.js"; diff --git a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts index 5944d4fcd2..c70c003237 100644 --- a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts @@ -3,7 +3,11 @@ import { metadataRowOffset, readManagedMetadataLayout, } from "../../../../src/dotnet/ManagedMetadataLayout.js"; -import { buildManagedNestedTypeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js"; +import { + buildManagedNestedTypeFixture, + buildManagedPeFixture, + managedPeFixtureTarget, +} from "../../../../src/dotnet/ManagedPe.fixture.js"; import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; @@ -14,10 +18,6 @@ import { } from "../../../../src/domain/managed/managedMemberComparison.js"; import { managedMemberInspectionSchema } from "../../../../src/domain/managed/managedArtifact.js"; import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { - buildManagedPeFixture, - managedPeFixtureTarget, -} from "../../../../src/dotnet/ManagedPe.fixture.js"; import { buildManagedPeFixture as buildMultiMethodFixture } from "../../../../scripts/fixtures/managed/pe.mjs"; import { diff --git a/tests/boundary/providers/native/lldbCallTracerFailures.test.ts b/tests/boundary/providers/native/lldbCallTracerFailures.test.ts index 3ba872d901..8cd5999666 100644 --- a/tests/boundary/providers/native/lldbCallTracerFailures.test.ts +++ b/tests/boundary/providers/native/lldbCallTracerFailures.test.ts @@ -9,8 +9,7 @@ import { type FileHandle, } from "node:fs/promises"; import { ChildProcess } from "node:child_process"; -import { dirname } from "node:path"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; import { describe, expect, it, onTestFinished, vi } from "vitest"; diff --git a/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts b/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts index 83368d9479..816747dbf2 100644 --- a/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts +++ b/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts @@ -14,7 +14,11 @@ import { } from "node:fs/promises"; import { dirname, join } from "node:path"; import { describe, expect, it, onTestFinished } from "vitest"; -import { signatureInspectionFixture } from "../../../fixtures/nativeCommands.js"; +import { + signatureInspectionFixture, + NativeFixtureRunner, + nativeMachoTargetForFile, +} from "../../../fixtures/nativeCommands.js"; import { resolvedAnalysisError } from "../../../../src/domain/analysisErrorBase.js"; import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; @@ -31,10 +35,6 @@ import { import { inspectSignatureSchema } from "../../../../src/domain/native/nativeInspection.js"; import { err, ok } from "../../../../src/domain/result.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; -import { - NativeFixtureRunner, - nativeMachoTargetForFile, -} from "../../../fixtures/nativeCommands.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; const CONTENT = "registered executable bytes"; diff --git a/tests/composition/reverseEngineeringEdgeCases.test.ts b/tests/composition/reverseEngineeringEdgeCases.test.ts index 95e0d14028..226284cc37 100644 --- a/tests/composition/reverseEngineeringEdgeCases.test.ts +++ b/tests/composition/reverseEngineeringEdgeCases.test.ts @@ -10,10 +10,12 @@ import { } from "../support/reverseEngineeringCompositionFixtures.js"; import { TestReverseEngineeringService as ReverseEngineeringService } from "../support/reverseEngineeringService.js"; -import { AnalysisCapabilityUnavailableError } from "../../src/domain/analysisErrorCore.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisResourceConstraintError, +} from "../../src/domain/analysisErrorCore.js"; import { ReverseEngineeringService as CoreReverseEngineeringService } from "../../src/application/reverse/ReverseEngineeringService.js"; import { createReverseEngineeringCommandPort } from "../../src/composition/reverseEngineering.js"; -import { AnalysisResourceConstraintError } from "../../src/domain/analysisErrorCore.js"; import { objdumpCommand } from "../../src/objdump/ObjdumpCommand.js"; import { OwnedCommandFailure, diff --git a/tests/fixtures/analysisView.ts b/tests/fixtures/analysisView.ts index 454526c1ce..af2e243551 100644 --- a/tests/fixtures/analysisView.ts +++ b/tests/fixtures/analysisView.ts @@ -5,8 +5,10 @@ import { environmentStateHar, } from "./historicalHar.js"; import { webNetworkCaptureSchema } from "../../src/domain/webNetworkCapture.js"; -import { BINARY_LAYOUT_TEST_PROVIDER } from "./binaryDiagnostics/layout.js"; -import { binaryLayoutFixture } from "./binaryDiagnostics/layout.js"; +import { + BINARY_LAYOUT_TEST_PROVIDER, + binaryLayoutFixture, +} from "./binaryDiagnostics/layout.js"; import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; import { createEvidence, type Evidence } from "../../src/domain/evidence.js"; import type { JavaScriptApplicationAnalysisResult } from "../../src/domain/javascript/javascriptApplicationAnalysis.js"; diff --git a/tests/process-global/apkSignatureLifecycle.test.ts b/tests/process-global/apkSignatureLifecycle.test.ts index bca360c835..407a61d82a 100644 --- a/tests/process-global/apkSignatureLifecycle.test.ts +++ b/tests/process-global/apkSignatureLifecycle.test.ts @@ -2,8 +2,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; -import { afterAll, afterEach, beforeAll, expect, it } from "vitest"; -import { vi } from "vitest"; +import { afterAll, afterEach, beforeAll, expect, it, vi } from "vitest"; const fault = vi.hoisted(() => ({ path: "", diff --git a/tests/process-global/derivedOperationFailures.test.ts b/tests/process-global/derivedOperationFailures.test.ts index f6d6640e88..9caa9b8e5c 100644 --- a/tests/process-global/derivedOperationFailures.test.ts +++ b/tests/process-global/derivedOperationFailures.test.ts @@ -38,7 +38,10 @@ import { processEvidence, } from "../../src/application/ReconstructionObligationLedger.fixture.js"; import { processCaptureSchema } from "../../src/domain/process/processCapture.js"; -import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; +import { + JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE, + JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, +} from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; import { javascriptApplicationAnalysisResultSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { javaScriptSemanticQueryResultSchema } from "../../src/domain/javascript/javascriptSemanticQuerySchemas.js"; import { @@ -51,7 +54,6 @@ import { applicationFeatureTraceResultSchema } from "../../src/domain/javascript import { ownedApplicationVersionComparisonResultSchema } from "../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { javaScriptExportShapeComparisonResultSchema } from "../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { sourceToBundleComparisonResultSchema } from "../../src/domain/javascript/sourceToBundleComparisonSchemas.js"; -import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; import { javascriptRuntimeReconciliationResultSchema } from "../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; import { browserCompletenessSchema } from "../../src/domain/browserCompleteness.js"; import { createElectronActiveEvidence } from "../../src/application/javascript/ElectronActiveEvidence.js"; diff --git a/tests/process-global/managedBundleReadGuards.test.ts b/tests/process-global/managedBundleReadGuards.test.ts index 4dec43d4b5..cca6a7f4c3 100644 --- a/tests/process-global/managedBundleReadGuards.test.ts +++ b/tests/process-global/managedBundleReadGuards.test.ts @@ -5,15 +5,15 @@ import { parseMcpToolError } from "../fixtures/mcpToolError.js"; import { loadNativeMatchingBundleSources } from "../../src/application/native/NativeMatchingBundleSources.js"; import { NativeMatchingAdmission } from "../../src/application/native/NativeMatchingAdmission.js"; import { matchNativeFunctionsInputSchema } from "../../src/contracts/native/functionMatchingContracts.js"; -import { createEvidence } from "../../src/domain/evidence.js"; -import { runProviderAnalysis } from "../../src/application/DirectAnalysis.js"; -import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js"; -import { parseConfig } from "../../src/config/parseConfig.js"; import { + createEvidence, isImmutableEvidence, parseEvidence, type Evidence, } from "../../src/domain/evidence.js"; +import { runProviderAnalysis } from "../../src/application/DirectAnalysis.js"; +import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js"; +import { parseConfig } from "../../src/config/parseConfig.js"; import { ok } from "../../src/domain/result.js"; import { observed } from "../fixtures/analysisExecution.js"; import { diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts index bf03c31f5a..f387c1f6c7 100644 --- a/tests/process-global/updateMaintenance.test.ts +++ b/tests/process-global/updateMaintenance.test.ts @@ -218,9 +218,11 @@ describe("maintenance activation and layered configuration", () => { }), ); + // The fallback row is also FreeBuff's registration, and FreeBuff is the + // setup action that rewrites it, so both views stay in maintenance scope. await expect( existingMaintenanceScope(home, entryPoint, process.env), - ).resolves.toMatchObject({ clients: ["zcode"] }); + ).resolves.toMatchObject({ clients: ["freebuff", "zcode"] }); }); it("selects an enabled registration inherited through layered OpenCode files", async () => { @@ -428,6 +430,93 @@ describe("existing REA integration maintenance", () => { }); }); +describe("shared registration maintenance", () => { + it("refreshes an owned shared registration without its product profile", async () => { + // FreeBuff's registration lives in the shared user agents file. The row is + // owned, so maintenance must refresh it even though no FreeBuff profile + // exists to establish product applicability. + await writeClient( + "freebuff", + JSON.stringify({ + mcpServers: { + other: { command: "other-server" }, + rea: { command: entryPoint, args: ["mcp"] }, + }, + }), + ); + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toEqual({ + clients: ["freebuff"], + skill: false, + skillDestinations: [], + }); + }); + + it("retains FreeBuff when ZCode reads the shared fallback", async () => { + // Both products load the one shared row, but their setup actions write + // different files: ZCode writes ~/.zcode/cli/config.json and deliberately + // leaves ~/.agents/mcp.json untouched (docs/client-configuration.md), so + // its fallback view cannot cover FreeBuff's registration. The shared row + // would stay pinned to the old version without FreeBuff's own action. + await writeClient("zcode", JSON.stringify({ mcp: { servers: {} } })); + await mkdir(join(home, ".config", "freebuff-desktop"), { + recursive: true, + }); + await writeClient( + "freebuff", + JSON.stringify({ + mcpServers: { + rea: { command: "npx", args: ["-y", "rea-agents@0.1.0", "mcp"] }, + }, + }), + ); + + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toMatchObject({ clients: ["freebuff", "zcode"] }); + }); + + it("leaves a foreign shared registration out of maintenance", async () => { + await writeClient( + "freebuff", + JSON.stringify({ + mcpServers: { rea: { command: "foreign-server", args: [] } }, + }), + ); + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toEqual({ + clients: [], + skill: false, + skillDestinations: [], + }); + }); + + it("leaves a ZCode-disabled shared row out of maintenance", async () => { + // The shared file doubles as ZCode's fallback, where `enable: false` + // disables the registration. Ownership evidence must not let maintenance + // rewrite the row as a FreeBuff registration and drop that flag. + const configPath = await writeClient( + "freebuff", + JSON.stringify({ + mcpServers: { + rea: { command: entryPoint, args: ["mcp"], enable: false }, + }, + }), + ); + const original = await readFile(configPath, "utf8"); + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toEqual({ + clients: [], + skill: false, + skillDestinations: [], + }); + expect(await readFile(configPath, "utf8")).toBe(original); + }); +}); + describe("updated executable maintenance planning", () => { it("runs real setup as a noninteractive dry run and creates no new integrations or backups", async () => { const configPath = await writeClient("codex", staleCodex);