From 69a2faa1207d99920599376f737327b2d06f7133 Mon Sep 17 00:00:00 2001 From: ezan Date: Sun, 25 Oct 2026 10:29:17 +0700 Subject: [PATCH] feat: add FreeBuff agent support for REA setup (#2736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add FreeBuff agent support for REA setup Register FreeBuff as a supported setup client using the shared user agents directory (~/.agents/mcp.json with mcpServers, skills in ~/.agents/skills), which matches what FreeBuff Desktop loads. Adds the client definition, boundary tests, supported-agent docs, and the skill reference client list. * fix(freebuff): detect FreeBuff from its profile directory, not shared .agents The ~/.agents marker made FreeBuff appear installed for any home where REA had installed shared skills (the default skill destination, also Gemini CLI's), producing false doctor failures. Detect FreeBuff from its own fixed profile directory ~/.config/freebuff-desktop instead, and add a regression test covering a home with only the shared agents skill directory. * fix(setup): require product evidence for FreeBuff discovery Address review findings at their producing boundaries and preserve directly affected consumers. Source inspection and git diff --check completed. Tests and automated checks were not run at the user’s explicit request. * fix(uninstall): retain unverified shared client configuration * fix(clients): preserve scoped removal evidence * fix(doctor): omit undefined selectedClientIds under exactOptionalPropertyTypes Passing selectedClientIds explicitly as undefined fails typecheck on this branch (TS2379), which blocks the focused test workflow before any test can run. Spread the option only when defined. * fix(update): separate ownership evidence from product applicability rea update's maintenance scope inspected only product applicability evidence, so when a FreeBuff profile was absent but REA already owned a version-pinned row in the shared ~/.agents/mcp.json, the plan omitted freebuff and left the integration stale. FreeBuff now declares its registration storage as shared, which proves ownership of an owned row but never product applicability, and maintenance requests ownership evidence through that declaration instead of reinterpreting the shared paths. Doctor and setup keep readiness evidence, foreign rows stay out of maintenance, and the Doctor exactOptionalPropertyTypes fix makes the focused test workflow runnable. * test(dotnet): split oversized operand extent diagnostics describe The describe callback reached 162 lines (max 150), failing max-lines-per-function and blocking the pre-push check for this branch. Move the single-case extent scenarios into their own describe; no test content or order changes. * fix(update): preserve ZCode's disabled shared registration during maintenance The shared user agents file doubles as ZCode's fallback, where `enable: false` disables the registration. Ownership-based maintenance read that row as a FreeBuff registration, whose format does not recognize the flag, so the row was selected and the emitted setup command rewrote it, dropping the flag and silently re-enabling a registration the user had disabled. registrationActivation now honors `enable: false` for any client whose registration storage is shared, because the activation fact binds every reader of that storage. Maintenance leaves such rows out; doctor reports them as stale, matching ZCode's own treatment. * fix(doctor): let explicit client selection authorize the config read When an ACL or an inaccessible shared parent made every access probe fail, a client selected by name was omitted before the configuration read could return its unreadable-config detail, so doctor reported it as not detected with installation remediation: an unresolved selected path collapsed to absence. An explicit selection now authorizes the read outright and the probes serve automatic discovery only. A selected client with an unreadable config reports invalid with the read failure; one with no config reports missing; automatic discovery still probes before inspecting. * fix(uninstall): share shared-storage checks between ZCode and FreeBuff clients() queued ~/.agents/mcp.json twice: once as zcode_fallback and once as freebuff. ZCode has no discoveryPaths, so its strict read still blocked uninstall on malformed shared text, despite FreeBuff's new retain-and-continue behavior. For a valid owned entry, ZCode removed it first, so FreeBuff reported skipped rather than removed. Make the shared file's checks and removal one shared decision: - Add zcodeDiscoveryPaths to ZCode so it has its own applicability evidence. - Gate zcode_fallback push on applicable, not just zcodeFallbackPath presence. - Report shared storage ownership as one decision for both clients: FreeBuff and ZCode now use clientOwnershipEvidencePaths together, so a recognizable REA-owned row proves ownership once for the shared storage, and both clients report the same result consistently. Co-Authored-By: Claude Opus 4.6 * fix(uninstall): merge duplicated host and prove shared ownership from owned rows Uninstall.ts declared systemUninstallHost twice, preventing module load, with the exported copy returning only clients and the second copy missing clientWriteBlocker. Merge into one exported host carrying both blockers. hasOwnershipEvidence used access() existence, so an unrelated malformed ~/.agents/mcp.json established ownership and blocked every removal. Ownership now requires a readable, recognizable REA-owned row via the injected filesystem, so unrelated shared data stays retained. Also cover ZCode discoveryPaths in its path test. * fix(uninstall): keep denied product-local configurations fail-closed Unproven product applicability marked every configuration path of that client unverified, so a denied ZCode profile turned an unreadable product-local config into a retained item: uninstall continued and could report complete with REA still registered. The retain-and-continue exception now applies only to storage REA does not own, classified once by clientSharedRegistrationPaths (shared configuration plus another product's same-scope fallback). Covers the Greptile P1 on Uninstall.ts. Validation: the new test fails on the pre-fix Uninstall and passes after it; freebuff/zcode/updateMaintenance/bom/dsh suites green; clientLifecycle, clientRegistrationStatus and claudeDesktop keep the same 8 host failures as the pre-merge baseline (symlink EPERM, stale-vs-aligned on Windows). * fix(update): retain FreeBuff when ZCode reads the shared fallback Maintenance deduplicated clients on the path each inspection read, so ZCode's fallback view of ~/.agents/mcp.json marked that file "covered" and dropped FreeBuff from the scope. ZCode setup writes ~/.zcode/cli/config.json and deliberately leaves the fallback untouched (docs/client-configuration.md), so the emitted command refreshed only ZCode while the shared row both products load stayed pinned to the old package version. Each setup action rewrites only its own configuration path and no other action rewrites the shared row, so every selected client keeps its own maintenance action; the read-path covered/deduped filter is removed. Covers the Codex P2 on UpdateMaintenance.ts:104. Validation: the new regression fails on e2fdec384 with {clients:["zcode"]} and passes with {clients:["freebuff","zcode"]}; updateMaintenance 31/31, setupClientConfigCleanupOwnership, cliUpdateAlias, updateOutput, cliStartupImports green; tsc --noEmit, oxlint (0 errors) and oxfmt --check clean; clientRegistrationStatus keeps the same 4 symlink/stale-vs-aligned failures this Windows host has on base e2fdec384 (verified by stashing the change). πŸ€– Generated with Codebuff Co-Authored-By: Codebuff * fix(uninstall): preserve completed reads as shared ownership evidence hasOwnershipEvidence caught every read failure and kept scanning, so a handle-close failure after a successful read β€” a RegularFileCleanupFailure whose completed outcome still contains the exact shared-file text β€” was discarded as "no proof". FreeBuff's applicability-unverified storage therefore never got its ownership fact, and once the retained handle blocked later reads, readClientConfiguration downgraded them to retained: inspection passed, removals soft-failed, and runUninstall(true) could purge REA data while the active shared registration remained. The catch now parses the completed outcome for the ownership text while missing or unreadable storage and malformed data keep their proves-nothing semantics, so ownership holds and every later read failure stays blocking. Covers the Codex P2 on Uninstall.ts:252. Validation: the new regression fails on 4dbfdf1a9 (freebuff reports no blocking failure while the poisoned-scope error lands on claude_code) and passes after; setupClientConfigCleanupOwnership 25/25; the other 21 Uninstall-consumer suites are byte-identical to base 4dbfdf1a9 (61 pre-existing Windows-host failures: symlink EPERM, stale-vs-aligned); tsc --noEmit, oxlint (0 errors, same 6 warnings) and oxfmt --check clean. πŸ€– Generated with Codebuff Co-Authored-By: Codebuff * fix(uninstall): keep owned-row proof through removal refusals hasOwnershipEvidence treated any clientRegistrationRemoval refusal as no ownership: an effective REA-owned rea member shadowed by an earlier foreign duplicate (shadowed_unowned_member), or an owned native row beside a foreign V1 fallback (foreign_legacy_fallback), discarded the already-parsed positive row fact. With FreeBuff unverified and close failures recurring on the preflight/removal rereads, those failures were then downgraded to retained, so runUninstall(true) removed the skill and purged REA data while the active owned row remained. The refusal now carries the owned rows that motivated it β€” the refusal only refuses the edit β€” and hasOwnershipEvidence evaluates that fact on both paths; preflight and removal keep refusing the unsafe edit exactly as before. Covers the Codex P2 on Uninstall.ts:269. Validation: the new regression fails on 225cb214a (freebuff reports no blocking failure while the poisoned-scope error lands on claude_code) and passes after; setupClientConfigCleanupOwnership 26/26; the 21 Uninstall-consumer suites are byte-identical to the recorded 225cb214a baseline (61 pre-existing Windows-host failures: symlink EPERM, stale-vs-aligned); tsc --noEmit, oxlint (0 errors, same 6 warnings) and oxfmt --check clean. πŸ€– Generated with Codebuff Co-Authored-By: Codebuff * fix(uninstall): keep unresolved discovery probes from softening shared failures The discovery probe swallowed every stat failure, so a FreeBuff profile denied with EACCES/EIO was indistinguishable from an absent profile: shared storage was marked applicability-unverified and, when ~/.agents/mcp.json also remained unreadable, the ownership and removal reads softened to retained. runUninstall(true) then removed the skill and purged REA data while an installed FreeBuff could still hold an active registration β€” the regression even reports complete. Absence (ENOENT) is now the only state that leaves shared paths unverified; any other probe failure leaves applicability unresolved, and shared-config failures stay blocking. Covers the Codex P2 on Uninstall.ts:318. Validation: the new regression fails on 7572a7082 with status complete and purged data and passes after; freebuffClientConfiguration 13/13; the Uninstall-consumer batches are byte-identical to the recorded baselines (61 pre-existing Windows-host failures: symlink EPERM, stale-vs-aligned); tsc --noEmit, oxlint (0 errors, same warning rules) and oxfmt --check clean. πŸ€– Generated with Codebuff Co-Authored-By: Codebuff * fix(uninstall): retain completed configuration reads as removal evidence A close failure after a completed read discarded the text that read produced, so an owned registration row the text proves softened to retained. The completed text now drives removal, keeping shared-config failures blocking when ownership evidence exists. Co-Authored-By: Anthropic * fix(uninstall): stop removal when a cleanup retry leaves a handle unreleased A completed reread whose handle the cleanup retry cannot release now fails blockingly instead of serving the text: with no owned row, the soft path let removeAll purge managed data before close() reported the incomplete cleanup. The completed text is served only after the retry releases the handle; an unreleased handle is REA's own failure, never applicability uncertainty, so it bypasses the retained softening. Co-Authored-By: Anthropic * fix(uninstall): keep the ZCode fallback when its profile probe is unresolved A denied or failed profile probe leaves ZCode's applicability unresolved, never absent, but the fallback branch dropped the shared row on applicability alone: with the native config and FreeBuff's profile both absent, an installed ZCode kept its uninspected registration while uninstall purged skill and data. Unresolved probes now keep the fallback in scope with the same fail-closed treatment as other unresolved shared paths; definitive absence still keeps ZCode out of another product's primary storage. Co-Authored-By: Anthropic * fix(setup): preserve a shared row's enable:false during FreeBuff repair The shared agents file doubles as ZCode's fallback, so its `enable: false` binds every reader: doctor now reports the disabled row as stale for FreeBuff, but following that remediation rebuilt the generic JSON entry without the flag and silently re-enabled a registration ZCode disabled. Repairs of shared storage now carry the activation fact forward, while a client's own native storage keeps its established single-reader repair contract; the denied-profile uninstall regression also pins the fallback's new fail-closed inspection beside the denied read. Co-Authored-By: Anthropic * test(uninstall): retarget the completed-reread regression to a released retry The concurrent-writer regression held the reread handle open forever, which the fail-closed path now serves: it duplicated the unreleased-handle regression while documenting the opposite contract. The fixture's close spy gains a failure limit so the cleanup retry releases the handle, and the test again proves the completed text drives removal of the concurrently added owned row, with purge verified against the cache layout purge actually removes. Co-Authored-By: Anthropic * fix(doctor): report a preserved shared disable as a manual conflict Preserving a shared row's enable:false left doctor recommending a setup run that registrationCurrent correctly treats as already current, so the scoped doctor returned needs_human forever. The shared-enable interpretation now has one owner, sharedEnableFlagBinds, used by both the activation rule and the remediation: a row disabled through the shared flag reports the manual conflict and states that rea setup will not change it, instead of an endlessly ineffective repair. Co-Authored-By: Anthropic * fix(doctor): keep a native ZCode disable on the setup repair path The manual-conflict remediation matched a native zcode row through sharedEnableFlagBinds, but setup drops enable:false on single-reader storage, so a native disabled registration was told setup would not change it. The remediation now reads setup's own writer rule, exported as setupPreservesDisabledActivation: shared storage and the zcode fallback view keep the manual-conflict message, while a native row keeps the working rea setup instruction. Regressions pin both remediations. πŸ€– Generated with Codebuff Co-Authored-By: Codebuff * revert: drop the unrelated managed-CIL describe split The split does not belong to FreeBuff client support and only widens this PR's diff. πŸ€– Generated with Codebuff Co-Authored-By: Codebuff * fix(uninstall): remove the shared agent registration once for both clients ZCode's ~/.agents/mcp.json fallback and FreeBuff's own configuration queued the same physical file twice, so ZCode removed the row and FreeBuff then reported it absent, losing the outcome for the client that owns the file. Resolve every configuration path to one owner -- the client whose configuration it is -- and project that single inspection and removal to each client that loads it. A fallback reader still contributes its discovery probe, so an installed or unresolved reader keeps the shared file fail-closed. Discovery also accepted a dangling ~/.config/freebuff-desktop link, which setup and doctor reject because they follow the target, so an unrelated malformed shared file blocked every other removal. Resolve the link before treating the profile as installation evidence, keeping lstat for removal safety. Also date the FreeBuff release wording against the published 6.7.0 package. * fix(uninstall): keep a row per client when two clients own one file OPENCODE_CONFIG can point OpenCode at FreeBuff's configuration file, where OpenCode writes `mcp.rea` and FreeBuff writes `mcpServers.rea`. Grouping by filename gave that path a single owner, so uninstall removed only OpenCode's row, reported that removal for FreeBuff, and could purge managed data with FreeBuff's registration still active. Share the decision only through a fallback view of another client's row: every client that declares the path as its own configuration keeps its own inspection and removal, and the fallback reader reports each of them. * fix(clients): preserve discovery and fallback registration identity Follow product discovery link targets without losing configuration entry presence or explicitly selected inspections. Associate fallback readers with the format owner's registration table and project outcomes by owner and path, so OpenCode removal cannot be reported as a ZCode fallback removal. All three new boundary regressions fail against the previous source and pass with the fixes. Validation: check:fast; 217 focused tests passed; the two changed test files were rerun with 50 passing tests. No installed FreeBuff client was launched. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Codebuff --- .../references/connection-and-recovery.md | 2 +- docs/client-configuration.md | 22 + docs/client-verification.md | 5 + docs/installation.md | 3 + src/adb/AdbProvider.ts | 8 +- src/apktool/ApktoolProvider.ts | 8 +- src/application/BinaryTargetResolver.ts | 12 +- .../BrowserObservationService.test.ts | 8 +- src/application/ClientRegistration.ts | 17 + src/application/ClientRegistrationStatus.ts | 70 ++- src/application/DirectAnalysis.ts | 6 +- src/application/Doctor.ts | 9 +- .../EnhancedTools.objcClasses.test.ts | 2 +- src/application/Setup.fixture.ts | 3 +- src/application/SetupHost.ts | 8 +- src/application/SupportedClients.ts | 108 +++- src/application/Uninstall.ts | 439 ++++++++++++--- src/application/UpdateMaintenance.ts | 12 + .../analysisView/AnalysisViewService.test.ts | 8 +- .../android/AndroidAnalysisPort.ts | 2 +- .../binary/AnalysisProviderRegistry.test.ts | 6 +- .../binary/AnalysisProviderRegistry.ts | 2 +- src/application/binary/BinarySession.ts | 2 +- src/application/binary/BinarySessionOpen.ts | 6 +- .../environment/environmentCompare.ts | 6 +- src/application/gpu/GpuCodeObjectsService.ts | 6 +- .../JavaScriptArtifactGraphContext.ts | 4 +- .../reverse/ReverseEngineeringService.ts | 2 +- .../sqlite/SqliteDatabaseService.test.ts | 3 +- src/artifacts/ArtifactResourceScope.test.ts | 6 +- .../extraction/ArtifactExtraction.ts | 10 +- .../PyInstallerArtifactReader.test.ts | 6 +- src/browser/CdpBrowserProvider.ts | 6 +- src/browser/CdpElectronInspection.test.ts | 3 +- src/browser/CdpScreenshot.ts | 6 +- .../PlaywrightBrowserScenarioProvider.test.ts | 6 +- .../WebBundleAnalysisWorkerServer.test.ts | 2 +- src/browser/WebSourceMapFetcher.ts | 6 +- src/browser/history/CaptureFailures.ts | 6 +- .../network/PlaywrightScenarioNetwork.ts | 2 +- .../native/binaryDiagnosticsToolContracts.ts | 10 +- src/contracts/toolOutputSchemaGroups.ts | 22 +- src/cutter/CutterBridgeClient.ts | 16 +- src/cutter/CutterBridgeService.ts | 2 +- src/domain/analysisSnapshot.ts | 3 +- src/domain/analysisView/analysisView.test.ts | 10 +- src/domain/android/androidApplication.ts | 3 +- src/domain/apple/appleApplication.ts | 3 +- src/domain/bundleComparison.ts | 3 +- src/domain/completionLedgerGeneration.ts | 3 +- src/domain/evidence.ts | 3 +- src/domain/functionComparisonSchemas.ts | 3 +- src/domain/harmony/harmonyApplication.ts | 3 +- .../javascriptFeatureTraceSchemas.ts | 3 +- .../javascriptRuntimeReconciliation.ts | 6 +- .../javascriptSemanticGraphQuery.test.ts | 2 +- .../javascriptSemanticGraphTraversal.test.ts | 6 +- .../javascript/javascriptSemanticQuery.ts | 2 +- .../javascriptSemanticQuerySchemas.ts | 3 +- .../javascriptSemanticRequireOrigin.ts | 6 +- ...avascriptSemanticSharedChildWrites.test.ts | 6 +- .../javascript/javascriptSourceAnalysis.ts | 6 +- .../sourceToBundleComparisonSchemas.ts | 3 +- src/domain/managed/managedApplicationGraph.ts | 3 +- src/domain/managed/managedMemberComparison.ts | 3 +- .../managedNativeVerificationSchemas.ts | 3 +- .../mobileApplicationGraphProjection.ts | 3 +- src/domain/reconstructionCoverage.ts | 3 +- .../reconstructionObligationLedgerSchemas.ts | 3 +- src/domain/reconstructionVerification.test.ts | 6 +- .../reconstructionVerificationSchemas.ts | 3 +- src/domain/referenceSourceGraph.ts | 3 +- src/domain/staticRuntimeCorrelation.ts | 6 +- src/evm/EvmoleInterfaceProvider.ts | 2 +- src/flutter/FlutterBuildProvider.ts | 8 +- src/ghidra/GhidraDiagnostics.test.ts | 6 +- src/ghidra/GhidraDolLoadImage.test.ts | 6 +- src/ghidra/GhidraInstallation.test.ts | 3 +- src/ghidra/GhidraInstallation.ts | 3 +- src/ghidra/GhidraInstallationProperties.ts | 6 +- src/ghidra/GhidraProvider.behavior.test.ts | 3 +- src/ghidra/GhidraProviderClient.ts | 16 +- src/ghidra/GhidraSnapshotCache.test.ts | 3 +- src/gpu/PtxKernels.ts | 6 +- src/hopper/HopperMachOImage.ts | 6 +- src/hopper/HopperProvider.ts | 8 +- src/ida/IdaNativeProtocol.test.ts | 6 +- src/ida/IdaProvider.ts | 12 +- .../analysis/JavaScriptAnalysisWorker.ts | 6 +- .../JavaScriptAnalysisWorkerServer.ts | 6 +- src/native/LldbCallTracer.ts | 6 +- src/native/NativeMacOSProvider.ts | 2 +- src/native/pwntools/PwntoolsDecoder.ts | 6 +- src/pcap/PcapConfiguration.ts | 6 +- src/pcap/PcapProvider.ts | 6 +- src/process/ArtifactAnalysisResources.test.ts | 6 +- .../capture/FilesystemCoverage.test.ts | 6 +- .../ProcessCaptureTerminalRetention.test.ts | 6 +- src/server/registerElectronTools.ts | 6 +- src/server/registerFridaTools.ts | 6 +- src/wasm/WabtArtifactProvider.ts | 4 +- .../tools/nativeFunctionMatching.test.ts | 2 +- tests/boundary/cli/versionReportCli.test.ts | 3 +- .../filesystem/clientLifecycle.test.ts | 1 + .../clientRegistrationStatus.test.ts | 72 +++ .../filesystem/flutterBuildProvider.test.ts | 2 +- .../freebuffClientConfiguration.test.ts | 469 ++++++++++++++++ .../ghidraProfileArtifactOwnership.test.ts | 3 +- .../electronApplicationAnalysis.test.ts | 6 +- .../javascriptApplicationSignedZero.test.ts | 6 +- .../javascriptRuntimeReconciliation.test.ts | 6 +- .../javascriptIntegrityPolicy.test.ts | 6 +- .../setupClientConfigCleanupOwnership.test.ts | 520 +++++++++++++++++- .../zcodeClientConfiguration.test.ts | 36 +- tests/boundary/mcp/devicetreeMcp.test.ts | 3 +- tests/boundary/mcp/electronMcp.test.ts | 3 +- ....01-lists-the-complete-target-open.test.ts | 3 +- tests/boundary/mcp/erlangBeamMcp.test.ts | 7 +- tests/boundary/mcp/ghidraMcpHarness.ts | 3 +- tests/boundary/mcp/ghidraReopen.test.ts | 3 +- tests/boundary/mcp/largeToolResult.test.ts | 2 +- .../boundary/mcp/nativeUiAvailability.test.ts | 2 +- tests/boundary/mcp/promptMcp.test.ts | 3 +- .../process/processPreparation.test.ts | 6 +- ...anagedMemberComparisonFromMetadata.test.ts | 10 +- .../native/lldbCallTracerFailures.test.ts | 3 +- .../nativeSignatureTargetBinding.test.ts | 10 +- .../reverseEngineeringEdgeCases.test.ts | 6 +- tests/fixtures/analysisView.ts | 6 +- .../apkSignatureLifecycle.test.ts | 3 +- .../derivedOperationFailures.test.ts | 6 +- .../managedBundleReadGuards.test.ts | 8 +- .../process-global/updateMaintenance.test.ts | 91 ++- 133 files changed, 2098 insertions(+), 389 deletions(-) create mode 100644 tests/boundary/filesystem/freebuffClientConfiguration.test.ts 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);