From c77735fa83a66552ec45eef435ab3bb36026ecfe Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:34:28 +0800 Subject: [PATCH] fix(setup)!: bind managed registrations to selected installation Use one command owner for setup, client writers, Gemini policy and CLI defaults. Migrate recognized runner registrations with backups and remove runner prewarm. Retain unknown identities and exercise real offline npm, CLI and MCP workflows. Fixes #7757 BREAKING CHANGE: managed MCP registrations use the selected absolute runtime and REA entry script, including package-runner cache paths. Keep that installation available or restore it before setup; unknown registrations require confirmed manual repair. Setup no longer returns prewarm or emits its progress events. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) --- .../skills/reverse-engineer-anything/SKILL.md | 7 +- .../references/connection-and-recovery.md | 5 +- README.md | 5 +- docs/client-configuration.md | 11 +- docs/installation.md | 93 ++--- scripts/lib/verify-package-runner-fixture.mjs | 8 + scripts/verify-package-environment.mjs | 8 +- scripts/verify-package-setup.mjs | 31 +- src/application/ClientRegistrationIdentity.ts | 49 ++- src/application/ClientRegistrationStatus.ts | 13 +- src/application/GeminiClientSettings.ts | 4 +- src/application/Setup.configuration.test.ts | 20 +- src/application/Setup.fixture.ts | 8 +- src/application/Setup.recovery.test.ts | 45 --- src/application/Setup.ts | 40 -- src/application/SetupClientConfiguration.ts | 11 +- src/application/SetupHost.ts | 53 +-- .../SetupRegistrationPrewarm.test.ts | 357 ------------------ src/application/SetupRegistrationPrewarm.ts | 259 ------------- src/application/SetupTypes.ts | 8 - src/cli.ts | 7 +- src/cli/clientRegistrationCommandText.ts | 5 + src/cli/interactiveSetup.ts | 2 - src/setupEnvironment.test.ts | 10 +- .../setup/codexToolPolicySetup.test.ts | 10 +- .../setup/duplicateJsonMemberSetup.test.ts | 41 +- tests/boundary/cli/clientProfiles.test.ts | 21 +- .../packageRunnerBoundInstallation.test.ts | 220 +++++++++++ .../cli/packageRunnerRegistration.test.ts | 39 +- .../claudeDesktopClientConfiguration.test.ts | 11 +- .../clientConfigurationDialects.test.ts | 12 +- .../clientConfigurationEnvironment.test.ts | 43 ++- ...entConfigurationLayeredEnvironment.test.ts | 9 +- .../clientRegistrationStatus.test.ts | 17 +- .../filesystem/codexClientPolicy.test.ts | 38 +- .../droidClientConfiguration.test.ts | 7 +- .../filesystem/dshClientConfiguration.test.ts | 7 +- .../freebuffClientConfiguration.test.ts | 7 +- .../hermesClientConfiguration.test.ts | 7 +- .../kimiClientConfiguration.test.ts | 28 +- .../filesystem/ompClientConfiguration.test.ts | 12 +- .../filesystem/piClientConfiguration.test.ts | 55 ++- .../qoderClientConfiguration.test.ts | 7 +- .../qwenClientConfiguration.test.ts | 22 +- tests/boundary/filesystem/setupConfig.test.ts | 28 +- .../filesystem/setupLauncherAlias.test.ts | 32 +- .../setupPackageRunnerBinding.test.ts | 32 ++ .../setupRegistrationPrewarm.test.ts | 220 ----------- .../traeClientConfiguration.test.ts | 7 +- .../filesystem/windowsNpxRegistration.test.ts | 85 +++-- .../workbuddyClientConfiguration.test.ts | 7 +- .../zcodeClientConfiguration.test.ts | 7 +- tests/conformance/readmeConsistency.test.ts | 7 +- .../conformance/releaseConfiguration.test.ts | 2 +- tests/fixtures/clientEnvironment.ts | 10 - tests/fixtures/codexToolPolicy.ts | 5 +- 56 files changed, 768 insertions(+), 1346 deletions(-) create mode 100644 scripts/lib/verify-package-runner-fixture.mjs delete mode 100644 src/application/SetupRegistrationPrewarm.test.ts delete mode 100644 src/application/SetupRegistrationPrewarm.ts create mode 100644 src/cli/clientRegistrationCommandText.ts create mode 100644 tests/boundary/cli/packageRunnerBoundInstallation.test.ts create mode 100644 tests/boundary/filesystem/setupPackageRunnerBinding.test.ts delete mode 100644 tests/boundary/filesystem/setupRegistrationPrewarm.test.ts diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index 9de416eb0..f0792352f 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -56,7 +56,7 @@ A plugin-only doctor result of `missing` does not call for `rea setup`. REA setup never installs or upgrades Node.js, npm, Homebrew, Java, Ghidra, IDA, JADX, Kuna, Binwalk, or Unblob. Use existing prerequisites; do not install unrelated software to repair MCP registration. For an unsupported client, use manual stdio -registration with a version-pinned `rea-agents` package or continue through the CLI. +registration with an existing installation’s absolute runtime and entry script, or continue through the CLI. A concrete CLI fallback for an operator-supplied JavaScript tree or ASAR is: @@ -69,7 +69,10 @@ graph, limitations, and unknowns with the same care as an MCP result; the CLI returns the Evidence record directly. This fallback does not establish that MCP is configured. Native CLI tasks still require their selected engine. Prefer an existing `rea` CLI. When none is installed and REA package acquisition -is within the authorized scope, use `npx -y rea-agents@latest` in its place. +is within the authorized scope, use `npx -y rea-agents@latest` in its place from a +trusted directory outside the target project, with absolute target paths. Managed +setup binds the current installation directly; retain its runtime and package +cache entry, or restore that installation before repairing its registration. For setup-owned registration, configuration repair starts with a read-only setup plan; show paths, backups and skill replacement before applying authorized changes. Preserve existing approval and target-execution boundaries. 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 7abd41491..d6491eab8 100644 --- a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md +++ b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md @@ -156,7 +156,10 @@ Use the current supported client's ID: `claude_code`, `claude_desktop`, `codex`, `copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `trae`, `grok_build`, `omp`, `pi`, `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. +For unsupported clients, use manual stdio registration with the selected +installation’s absolute runtime and entry script, or continue through the CLI. +Acquire packages from a trusted directory outside the target project. Keep a +package-runner installation and its runtime available while registrations use it. Show the plan's exact paths, backups, changes and bundled-skill replacement. Obtain approval for writes when that concrete scope is not already authorized, diff --git a/README.md b/README.md index 07af6c4d1..aee8f3fb6 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,10 @@ npx rea-agents setup Choose your agents, review the proposed changes, and approve them. Setup adds REA's MCP server and matching workflow instructions, with backups of existing -configuration. Restart your agent afterward. +configuration. Restart your agent afterward. Setup records the selected runtime and REA +entry script directly. If acquired through a package runner, keep its cache entry +available. Run package acquisition from a trusted directory outside the target +project. Installation and recovery details are in the setup guide below. Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build and [other agents](docs/installation.md#supported-agents). See diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 790e5a463..5e7ab056c 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -14,12 +14,17 @@ interpreter: the npm launcher's `#!/usr/bin/env node` still searches `PATH` for Node. Running that command successfully in a terminal does not establish that the client can launch it. -For direct JavaScript installations, setup registers the selected runtime +For JavaScript installations, setup registers the selected runtime executable and REA script by absolute path. On POSIX, it prefers executable `node` or `bun` and REA aliases on setup's `PATH` only when they resolve to the same selected files. This preserves manager-owned aliases across version updates; -otherwise setup uses the selected paths directly. Package-runner registrations keep -their selected runner. Setup does not copy your ambient environment into client +otherwise setup uses the selected paths directly. This also applies when setup +runs through a package runner: retain the selected cache entry and runtime while +the registration uses them. Clearing either makes startup fail without resolving +another package from the client's project. Restore the same installation before +repairing it; an unidentified registration remains untouched. See +[installation and recovery](installation.md#package-runners-and-global-installation). +Setup does not copy your ambient environment into client settings. To refresh an older registration, preview `rea setup --client --skill=false --dry-run`, then run the same command without `--dry-run` and approve the displayed changes. Setup preserves unrelated diff --git a/docs/installation.md b/docs/installation.md index 0868aabeb..49004b735 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -273,7 +273,7 @@ After reviewing an update plan, rerun that same prefixed setup command without `--dry-run` and approve its exact changes. Profile removal likewise needs the prefix on `claude mcp remove --scope user rea` and any `rea uninstall` command; keep the broader client scope of uninstall in mind. Restart the selected -profile after changing its pinned registration. +profile after changing its registration. `claude mcp remove` removes only the registration. To remove REA from this profile without running the broader uninstall flow, first back up the profile's @@ -323,30 +323,26 @@ commands. Record an existing selection first if you intend to restore it after finishing. The configuration path below is the default profile's `~/.claude.json`; a different active profile uses its own config root instead. -Register with the variable unset: - - +Register with the variable unset, substituting the absolute runtime and entry +script from your existing installation or setup dry-run: ```bash -claude mcp add --scope local rea -- npx -y rea-agents@6.8.0 mcp +claude mcp add --scope local rea -- "/absolute/path/to/node" "/absolute/path/to/rea/scripts/rea.mjs" mcp ``` -On native Windows, use the same project directory and wrap the package-runner -shim so the stdio client can launch it: +On native Windows, use the same project directory and absolute paths: ```powershell -claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.8.0 mcp +claude mcp add --scope local rea -- "C:\Program Files\nodejs\node.exe" "C:\tools\rea\scripts\rea.mjs" mcp ``` - - Local scope stores the entry under that directory's path in `~/.claude.json`, so other projects don't load it. This route doesn't install the bundled skill. Avoid `--scope project`, which writes `.mcp.json` into the repository where it can be committed or copied. `rea doctor` and `rea uninstall` read only the user-level entry, so they don't report or remove a local-scope registration; remove it with `claude mcp remove --scope local rea` from the same directory. -`rea update` doesn't refresh it either, so it stays on the version it names. +`rea update` doesn't refresh it either, so it continues using the selected installation. To move it to a newer release, first back up the project entry in `~/.claude.json` and record its environment and other metadata. In particular, preserve caller-selected overrides such as `REA_MCP_INPUT_SCHEMA_PROFILE` and @@ -553,8 +549,8 @@ non-TTY, and JSON modes without applying configuration changes. ## Released package and main Repository main documents its current code and generated catalog. `@latest` -selects the npm release, and persistent MCP registrations are pinned to the -version that performed setup. Installing newer instructions does not update a +selects the npm release, and managed MCP registrations bind the installation +that performed setup. Installing newer instructions does not update a running server or its registration. The npm release checked on **2026-10-11** was **6.4.0**, published from @@ -1101,14 +1097,15 @@ rea providers --json ``` `rea setup` can configure supported Windows agents and install the bundled -REA skill after approval. Direct registrations use Node to launch REA's entry -script; package-runner registrations use the pinned `npx` or `bunx` command. Hopper +REA skill after approval. Managed registrations use the absolute selected Node or +Bun runtime and REA entry script, including when setup ran through a package runner. Hopper installation remains unavailable on Windows. Setup never installs Ghidra, Java, or Python. It preserves valid detected Ghidra/JDK settings in agent registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics. -The separate `rea mcp add` builtin currently uses Incur's package-runner child -launcher, which can fail on Windows with `MCP_ADD_FAILED: spawn npx ENOENT` +The separate `rea mcp add` builtin delegates most clients to Incur's package-runner +child launcher. Run that acquisition from a trusted directory; prefer managed +`rea setup` when supported. That child launcher can fail on Windows with `MCP_ADD_FAILED: spawn npx ENOENT` before registering a client. REA's setup path is unaffected. For a client supported by setup, review `rea setup --client opencode --skill=false --dry-run --json` (substitute its client ID), then use the same scoped command without `--dry-run` @@ -1653,40 +1650,45 @@ For a client that supports Registry discovery, search for `io.github.morluto/rea and select the npm package. The published metadata launches the existing stdio server through `npx` with the `mcp` command. -For a client that requires manual configuration, use: +For supported clients, prefer `rea setup`: it records the absolute JavaScript +runtime and entry script of the REA installation performing setup. Client starts +then use that installation directly, keeping the client's working directory +without running npm or Bun package resolution there. Registry clients control +their own package acquisition; Registry metadata cannot name local absolute paths. - +For manual stdio configuration, substitute the absolute paths of an existing +installation (use the command shown in a setup dry-run): ```json { "mcpServers": { "rea": { - "command": "npx", - "args": ["-y", "rea-agents@6.8.0", "mcp"] + "command": "/absolute/path/to/node", + "args": ["/absolute/path/to/rea/scripts/rea.mjs", "mcp"] } } } ``` - +Run current setup to migrate recognized older `npx`/`bunx` registrations, with +backups, then restart the client. Unrelated or unidentified registrations remain +untouched. Setup does not prewarm a separate package-runner command. -Persistent registrations should use one exact package version. `rea setup` -writes the same package-runner shape, or `bunx rea-agents@ mcp` when -setup runs through `bunx`, and pins it to the exact version that performed -setup. Run current setup to refresh an older registration, then restart the -client. - -Package runners key their package cache by the requested spec, so the entry -installed for the `@latest` invocation that ran setup cannot serve the pinned -registration. Setup therefore materializes the pinned entry once, best-effort -with a bounded timeout, before finishing. When that step cannot run, for -example offline, the first client start performs the one-time install inside -the client's MCP startup deadline and may need one more restart. +If setup itself runs from a package-runner cache, the registration depends on +that cache entry and the selected runtime remaining installed. Removing either +makes startup fail; it does not download or resolve another package. Restore the +same installation and rerun setup. Changing package requests, cache roots, or +runtimes can create a different installation identity. If the old entry cannot be +identified, restore it first or manually remove the confirmed REA registration +before running setup again. A global installation provides a more durable path. ## Package runners and global installation -A package-runner invocation can use a REA version installed in the current project. To -request the latest release explicitly, use: +Package runners can select project-local packages and read project configuration, +even for explicit version requests. Acquire REA from a trusted working directory, +outside the target project, and pass absolute target paths to analysis commands. +REA cannot validate package acquisition that happens before its entrypoint starts. +To request the latest release explicitly, use: ```bash npx rea-agents@latest setup @@ -1703,16 +1705,14 @@ For an intentional rollback, make the package request explicit: npm exec --yes --package=rea-agents@2.4.0 -- rea setup ``` -Setup continues to pin persistent MCP registrations to the exact version that -performed setup. Running current setup later migrates unversioned or older -managed registrations through the normal reviewed setup transaction. +Setup binds persistent MCP registrations to the installation that performed setup. +Running current setup later migrates recognized legacy package-runner registrations +through the normal reviewed setup transaction. With Bun, `bunx rea-agents setup` (or `bunx rea-agents@latest setup`) is the -equivalent entry point. Setup run through `npx` registers -`npx -y rea-agents@ mcp`; setup run through `bunx` or `bun x` registers -`bunx rea-agents@ mcp`. On Windows, setup wraps either command in -`cmd.exe /d /c` so clients can start the runner's command shim. Neither -registration points into the runner's temporary package cache. +equivalent acquisition entry point. Managed registrations use the selected Bun +executable and REA entry script directly. Windows registrations likewise use +absolute runtime and script paths, without a package-runner command shim. REA supports Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), and 26.12.1+. Node.js 23, 25, earlier 26.x releases, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew. @@ -1746,8 +1746,9 @@ Review the plan, approve the selected client changes, then restart those clients This registers the installed entrypoint directly; installing the global package alone does not replace existing `npx` registrations. Each client still has its own REA process and analysis state. Direct registrations follow the installed CLI; -use `rea update` to update it. Running setup through `npx` again selects the pinned -package-runner command. Do not register paths inside npm's temporary package cache. +use `rea update` to update it. Running setup through `npx` again selects the +installation that executes setup, potentially in npm's temporary package cache; +retain that cache entry for as long as the registration uses it. ### npm workspace errors before REA starts diff --git a/scripts/lib/verify-package-runner-fixture.mjs b/scripts/lib/verify-package-runner-fixture.mjs new file mode 100644 index 000000000..a1833de84 --- /dev/null +++ b/scripts/lib/verify-package-runner-fixture.mjs @@ -0,0 +1,8 @@ +import { writeFileSync } from "node:fs"; + +// Keep argument boundaries intact so verifier consumers never parse shell display text. +writeFileSync( + process.env.REA_NPX_LOG, + JSON.stringify(process.argv.slice(2)) + "\n", +); +process.stdout.write("│ ✓ Cursor: isolated │\n"); diff --git a/scripts/verify-package-environment.mjs b/scripts/verify-package-environment.mjs index be2e81b86..91834d3fa 100644 --- a/scripts/verify-package-environment.mjs +++ b/scripts/verify-package-environment.mjs @@ -1,6 +1,6 @@ import { chmod, mkdir, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { supportedClients } from "../dist/application/SupportedClients.js"; /** Create an isolated prefix, fake PATH, client configs, and environment. */ @@ -41,7 +41,7 @@ export async function verifyPackageEnvironment({ await writeFile(swVers, "#!/bin/sh\necho 14.5\n"); await writeFile( npx, - '#!/bin/sh\nprintf "%s\\n" "$*" > "$REA_NPX_LOG"\necho "│ ✓ Cursor: isolated │"\n', + '#!/bin/sh\nexec "$REA_VERIFY_NODE" "$REA_VERIFY_NPX_SCRIPT" "$@"\n', ); await writeFile(hopper, "#!/bin/sh\nexit 0\n"); await Promise.all([ @@ -98,6 +98,10 @@ export async function verifyPackageEnvironment({ REA_ANALYSIS_PROVIDER: "auto", ...packageHopperEnvironment(root), REA_NPX_LOG: npxLog, + REA_VERIFY_NODE: process.execPath, + REA_VERIFY_NPX_SCRIPT: fileURLToPath( + new URL("./lib/verify-package-runner-fixture.mjs", import.meta.url), + ), REA_REFERENCE_SECRET_PATTERNS_JSON: JSON.stringify([".env"]), }; return { diff --git a/scripts/verify-package-setup.mjs b/scripts/verify-package-setup.mjs index 5dcd6490d..f11af1e38 100644 --- a/scripts/verify-package-setup.mjs +++ b/scripts/verify-package-setup.mjs @@ -1,3 +1,4 @@ +import { deepStrictEqual } from "node:assert"; import { lstat, mkdir, @@ -11,6 +12,7 @@ import { import { dirname, isAbsolute, join } from "node:path"; import { parse as parseToml } from "smol-toml"; +import { clientRegistrationCommandText } from "../dist/cli/clientRegistrationCommandText.js"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; import { PRODUCT_IDENTITY } from "../dist/identity.js"; import { pathExists } from "./lib/verify-package-core.mjs"; @@ -50,12 +52,29 @@ const isDirectRegistration = async (command, cli) => (await realpath(command[0])) === (await realpath(process.execPath)) && (await realpath(command[1])) === (await realpath(cli)); -const verifyMcpAdd = async ({ commands, cli, environment, npxLog }) => { +const verifyMcpAdd = async ({ commands, cli, environment, npxLog, home }) => { await commands.run(cli, ["mcp", "add"], environment); - const mcpRegistration = await readFile(npxLog, "utf8"); - const expected = `add-mcp npx -y ${PRODUCT_IDENTITY.registrationPackageSpecifier} mcp --name rea`; - if (!mcpRegistration.includes(expected)) - throw new Error("Incur mcp add did not register the pinned npx command"); + const settings = JSON.parse( + await readFile(join(home, ".config", "amp", "settings.json"), "utf8"), + ); + const registration = settings?.["amp.mcpServers"]?.rea; + if (!Array.isArray(registration?.args)) + throw new Error("Incur mcp add did not write the native Amp registration"); + const command = [registration.command, ...registration.args]; + if (!(await isDirectRegistration(command, cli))) + throw new Error( + "Incur mcp add did not register the selected absolute runtime and REA script", + ); + // Amp consumes the same command through Incur's parser. Check the delegated + // helper receives that command as one argument, without flattening its argv. + deepStrictEqual(JSON.parse(await readFile(npxLog, "utf8")), [ + "add-mcp", + clientRegistrationCommandText(command), + "--name", + "rea", + "-y", + "-g", + ]); }; const verifySetupPlan = async ({ @@ -588,7 +607,7 @@ export async function verifyPackageSetup({ hopperSetupSupported, root, }) { - await verifyMcpAdd({ commands, cli, environment, npxLog }); + await verifyMcpAdd({ commands, cli, environment, npxLog, home }); const skillPath = join( home, ".agents/skills/reverse-engineer-anything/SKILL.md", diff --git a/src/application/ClientRegistrationIdentity.ts b/src/application/ClientRegistrationIdentity.ts index e439e1188..e8f246328 100644 --- a/src/application/ClientRegistrationIdentity.ts +++ b/src/application/ClientRegistrationIdentity.ts @@ -1,6 +1,9 @@ import { accessSync, constants, realpathSync } from "node:fs"; import { isAbsolute } from "node:path"; -import { normalizeEntrypointPath } from "../process/CurrentEntrypointPath.js"; +import { + CURRENT_ENTRYPOINT_PATH, + normalizeEntrypointPath, +} from "../process/CurrentEntrypointPath.js"; import { valid } from "semver"; import { @@ -10,8 +13,8 @@ import { import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { PRODUCT_IDENTITY } from "../identity.js"; -/** Package runners whose pinned launcher setup can register. */ -export type PackageRunner = "npx" | "bunx"; +/** Package runners recognized for invocation guidance and legacy registrations. */ +type PackageRunner = "npx" | "bunx"; /** Read one variable; Windows variable names are case-insensitive. */ const environmentValue = ( @@ -57,24 +60,32 @@ export const withoutPackageRunnerIdentity = ( ), ); -/** Resolve a pinned package-runner launcher that stdio clients can spawn on the host. */ -export const packageRunnerRegistrationCommand = ( - runner: PackageRunner, - platform: NodeJS.Platform = process.platform, +/** Bind the selected REA entrypoint to the runtime already executing setup. */ +export const currentClientRegistrationCommand = ( + selection: { + readonly platform?: NodeJS.Platform; + readonly entrypointPath?: string | undefined; + readonly environment?: Readonly; + } = {}, ): readonly string[] => { - const command = - runner === "npx" - ? ["npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"] - : ["bunx", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"]; - // Both runners may be .cmd shims on Windows, which clients cannot spawn directly. - return platform === "win32" ? ["cmd.exe", "/d", "/c", ...command] : command; + const platform = selection.platform ?? process.platform; + const environment = selection.environment ?? process.env; + const entrypointPath = Object.hasOwn(selection, "entrypointPath") + ? selection.entrypointPath + : CURRENT_ENTRYPOINT_PATH; + if (entrypointPath === undefined || entrypointPath.length === 0) return []; + return [ + registrationRuntimePath( + process.execPath, + javaScriptRuntime(process.versions).id, + environment, + platform, + ), + registrationEntrypointPath(entrypointPath, environment, platform), + "mcp", + ]; }; -/** Resolve a pinned npx launcher that stdio clients can spawn on the host. */ -export const npxRegistrationCommand = ( - platform: NodeJS.Platform = process.platform, -): readonly string[] => packageRunnerRegistrationCommand("npx", platform); - const unwrapCmdCommand = (command: readonly string[]): readonly string[] => { const executable = command[0]?.toLowerCase(); if (executable !== "cmd" && executable !== "cmd.exe") return command; @@ -170,7 +181,7 @@ const sameFileExecutableAlias = ( }; /** Prefer an executable PATH alias that proves the same selected REA file. */ -export const registrationEntrypointPath = ( +const registrationEntrypointPath = ( entrypoint: string, environment: Readonly, platform: NodeJS.Platform, diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts index c30b5755e..24266348a 100644 --- a/src/application/ClientRegistrationStatus.ts +++ b/src/application/ClientRegistrationStatus.ts @@ -24,7 +24,6 @@ import { inspectClientRegistrationCommand, type ClientRegistrationCommandFacts, type ClientRegistrationCommandIdentity, - packageRunnerRegistrationCommand, } from "./ClientRegistrationIdentity.js"; import { clientEvidencePaths, @@ -470,17 +469,7 @@ const registrationAligned = ({ (registration.serverName !== "rea" || registration.transport !== "stdio") ) return "stale"; - if ( - (["npx", "bunx"] as const).some((runner) => { - const expected = packageRunnerRegistrationCommand(runner, platform); - return ( - command.length === expected.length && - command.every((argument, index) => argument === expected[index]) - ); - }) - ) - return "aligned"; - // Other owned package references remain stale until pinned to this version. + // Legacy package runners remain owned, but setup migrates them to the selected installation. if (commandFacts.package !== undefined) return "stale"; // Owning the script does not establish how its shebang interpreter resolves // in the client. Setup migrates these launchers without losing ownership. diff --git a/src/application/GeminiClientSettings.ts b/src/application/GeminiClientSettings.ts index 55a0bb9b4..a9398d162 100644 --- a/src/application/GeminiClientSettings.ts +++ b/src/application/GeminiClientSettings.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import { err, ok, type Result } from "../domain/result.js"; import { PRODUCT_IDENTITY } from "../identity.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; -import { npxRegistrationCommand } from "./ClientRegistrationIdentity.js"; +import { currentClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { clientConfigurationValuesEqual, clientRegistrationEntry, @@ -211,7 +211,7 @@ export const readClientPolicyBlock = async ( const desired = desiredRegistration ?? userDocument?.servers[PRODUCT_IDENTITY.mcpServerKey] ?? - clientRegistrationEntry("json", npxRegistrationCommand(), {}); + clientRegistrationEntry("json", currentClientRegistrationCommand(), {}); if ( override !== undefined && !clientConfigurationValuesEqual( diff --git a/src/application/Setup.configuration.test.ts b/src/application/Setup.configuration.test.ts index 6a671253d..c502299ff 100644 --- a/src/application/Setup.configuration.test.ts +++ b/src/application/Setup.configuration.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { FakeSetupHost, options } from "./Setup.fixture.js"; import { runSetup } from "./Setup.js"; -import { setupRegistrationCommand } from "./SetupHost.js"; +import { currentClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js"; import { discoverSetupState } from "./SetupPlan.js"; import type { MsixPackageView } from "./MsixPackageStorage.js"; @@ -414,13 +414,23 @@ describe("setup workflow action selection", () => { expect(result.status).toBe("ready"); expect(host.configuredCommands).toEqual([ - setupRegistrationCommand("win32", undefined, CURRENT_ENTRYPOINT_PATH), + currentClientRegistrationCommand({ + platform: "win32", + entrypointPath: CURRENT_ENTRYPOINT_PATH, + }), ]); expect( - setupRegistrationCommand("win32", undefined, CURRENT_ENTRYPOINT_PATH), + currentClientRegistrationCommand({ + platform: "win32", + entrypointPath: CURRENT_ENTRYPOINT_PATH, + }), ).toEqual([process.execPath, CURRENT_ENTRYPOINT_PATH, "mcp"]); - expect(setupRegistrationCommand("win32", undefined, undefined)).toEqual([]); - expect(setupRegistrationCommand("win32", "npx", undefined)).not.toEqual([]); + expect( + currentClientRegistrationCommand({ + platform: "win32", + entry