[GH-ISSUE #7321] [Bug] Uninstall misses the OpenCode V1 fallback preflight when entries occupy different user files #4349

Closed
opened 2026-10-31 00:49:06 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 26, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7321

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Setup, packaging, or upgrade

Observed behavior

rea uninstall --json exits 0 and reports complete/OpenCode removed when an owned native registration in opencode.jsonc shadows an unowned V1 registration in opencode.json. It deletes the native row and retains the V1 row. REA's production registration reader selects the owned command before uninstall and the retained V1 command afterward. Both rows in one file instead produce the existing preflight refusal and no change. Three isolated layered runs reproduced this; no configured launcher or native OpenCode process was executed.

Reproduction

From the source checkout below with its runtime already built and pinned Node active, run this standalone shell reproduction. It creates disposable configuration files, isolates HOME/configuration/cwd/environment, invokes the public CLI, and reads registration facts without executing the registered commands.

REA_CHECKOUT="$PWD"
NODE="$(command -v node)"
WORK="$(mktemp -d)"
mkdir -p "$WORK/home/.config/opencode" "$WORK/cwd" "$WORK/tmp" "$WORK/bin"
cat > "$WORK/home/.config/opencode/opencode.json" <<'JSON'
{"mcp":{"rea":{"type":"local","command":["/usr/bin/false"],"enabled":true}}}
JSON
cat > "$WORK/home/.config/opencode/opencode.jsonc" <<'JSON'
{"mcp":{"servers":{"rea":{"type":"local","command":["npx","-y","rea-agents@6.8.0","mcp"],"disabled":false}}}}
JSON
cd "$WORK/cwd"
run_node() {
  env -i HOME="$WORK/home" USERPROFILE="$WORK/home" \
    XDG_CONFIG_HOME="$WORK/home/.config" XDG_CACHE_HOME="$WORK/home/.cache" \
    XDG_DATA_HOME="$WORK/home/.local/share" TMPDIR="$WORK/tmp" PATH="$WORK/bin" \
    GEMINI_CLI_SYSTEM_SETTINGS_PATH="$WORK/no-system.json" \
    GEMINI_CLI_SYSTEM_DEFAULTS_PATH="$WORK/no-defaults.json" \
    REA_LOG_LEVEL=silent "$NODE" "$@"
}
registration() {
  run_node --input-type=module - "$REA_CHECKOUT" <<'JS'
import { pathToFileURL } from 'node:url';
const { readClientRegistrationStatuses } = await import(pathToFileURL(
  process.argv[2] + '/dist/application/ClientRegistrationStatus.js'));
const status = (await readClientRegistrationStatuses(process.env.HOME, undefined,
  {environment:process.env, selectedClientIds:['opencode']})).find(s=>s.client==='opencode');
console.log(JSON.stringify({command:status.command,state:status.state}));
JS
}
registration
run_node "$REA_CHECKOUT/scripts/rea.mjs" uninstall --json > "$WORK/uninstall.json"
echo "uninstall exit: $?"
run_node -e 'const r=require(process.argv[1]);const o=r.items.find(i=>i.name==="opencode");console.log(JSON.stringify({status:r.status,opencode:o.status,actions:o.actions.map(a=>a.status)}))' "$WORK/uninstall.json"
registration
cat "$WORK/home/.config/opencode/opencode.json" "$WORK/home/.config/opencode/opencode.jsonc"

Expected behavior

The documented uninstall rule says an unowned OpenCode V1 entry that removing V2 would expose stops preflight before any removal. These are supported user configuration layers. Uninstall should refuse this case before changing the native registration, as it already does when both rows occupy one file.

Impact and scope

The documented preflight protection against changing which registration is selected fails for this two-file layout. Uninstall reports success while REA's registration reader still resolves the retained V1 command. The lower file remains unchanged and the upper file has an empty native server table. Manually removing the unwanted lower V1 row before uninstall avoids the failure; the control without that row completed with registration missing. Native OpenCode startup and population frequency are NOT_ENOUGH_INFO; neither was measured. No purge-data consequence is claimed.

REA version

6.8.0 source, baseline ace4dbf36e8c8f36a0e192aff9a11484ed428358, using the built checkout runtime.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 arm64. No analysis provider or engine involved.

Target and artifact context

Two authored OpenCode user configuration files; no binary target or external artifact.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The standalone reproduction printed:

{"command":["npx","-y","rea-agents@6.8.0","mcp"],"state":"aligned"}
uninstall exit: 0
{"status":"complete","opencode":"removed","actions":["skipped","retained","removed"]}
{"command":["/usr/bin/false"],"state":"stale"}
{"mcp":{"rea":{"type":"local","command":["/usr/bin/false"],"enabled":true}}}
{"mcp":{"servers":{}}}
  • Expected source: docs/installation.md:447-451 promises preflight refusal for an unowned V1 entry exposed by native removal; docs/client-configuration.md:660-669 documents merged user configuration and uninstall across applicable files.
  • Failure source: the public CLI output and resulting files above, plus the executed production registration reader. Source tracing shows the preflight host inspects one candidate file at a time (src/application/Uninstall.ts:461-550); its fallback refusal considers the parsed document's legacy entry (src/application/Uninstall.ts:774-826). The effective registration reader instead merges applicable files (src/application/ClientConfigurationFiles.ts:178-232, consumed at src/application/ClientRegistrationStatus.ts:183).
  • Evidence provenance: observed for CLI status, file contents, and registration selection; static for the explanatory call path.
  • Local verification: reproduced, including execution of the standalone reproduction above.
  • Reproduction completeness: complete for REA's documented preflight behavior; native-client execution is outside the demonstrated result.

Additional context

Restoration check: with the same layered input, uninstall should report the V1 fallback refusal and leave both configuration files byte-identical. Retain the discriminating controls: both entries in one file already exit 1 with the fallback diagnostic and unchanged bytes; omitting the lower V1 row allows exit 0, removes the owned upper row, and leaves registration missing with command []. Both controls were executed.

Related history was read, including comments and closure explanations. #4977 was superseded by a main fix whose confirmed fallback controls concern entries in one document; that case passes here while the cross-file case fails. #5079 fixed lost reporting after a later file fails; this run preserves its per-file actions and has no removal failure. #864 introduced V2/V1 handling, and #6587 concerns inherited native setup dialect. No matching open cross-file preflight report was found.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A supported two-file configuration defeats a documented uninstall preflight refusal, removes the owned native row, and changes REA's selected registration while reporting success. The observed consequence is bounded and reversible; native execution was not tested.
  • Workaround status: available
  • Workaround basis: Remove the unwanted lower V1 row before uninstall. The executed control without that row completed removal and returned registration missing with no command.
Originally created by @coygeek on GitHub (Oct 26, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7321 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Setup, packaging, or upgrade ## Observed behavior `rea uninstall --json` exits 0 and reports `complete`/OpenCode `removed` when an owned native registration in `opencode.jsonc` shadows an unowned V1 registration in `opencode.json`. It deletes the native row and retains the V1 row. REA's production registration reader selects the owned command before uninstall and the retained V1 command afterward. Both rows in one file instead produce the existing preflight refusal and no change. Three isolated layered runs reproduced this; no configured launcher or native OpenCode process was executed. ## Reproduction From the source checkout below with its runtime already built and pinned Node active, run this standalone shell reproduction. It creates disposable configuration files, isolates HOME/configuration/cwd/environment, invokes the public CLI, and reads registration facts without executing the registered commands. ```sh REA_CHECKOUT="$PWD" NODE="$(command -v node)" WORK="$(mktemp -d)" mkdir -p "$WORK/home/.config/opencode" "$WORK/cwd" "$WORK/tmp" "$WORK/bin" cat > "$WORK/home/.config/opencode/opencode.json" <<'JSON' {"mcp":{"rea":{"type":"local","command":["/usr/bin/false"],"enabled":true}}} JSON cat > "$WORK/home/.config/opencode/opencode.jsonc" <<'JSON' {"mcp":{"servers":{"rea":{"type":"local","command":["npx","-y","rea-agents@6.8.0","mcp"],"disabled":false}}}} JSON cd "$WORK/cwd" run_node() { env -i HOME="$WORK/home" USERPROFILE="$WORK/home" \ XDG_CONFIG_HOME="$WORK/home/.config" XDG_CACHE_HOME="$WORK/home/.cache" \ XDG_DATA_HOME="$WORK/home/.local/share" TMPDIR="$WORK/tmp" PATH="$WORK/bin" \ GEMINI_CLI_SYSTEM_SETTINGS_PATH="$WORK/no-system.json" \ GEMINI_CLI_SYSTEM_DEFAULTS_PATH="$WORK/no-defaults.json" \ REA_LOG_LEVEL=silent "$NODE" "$@" } registration() { run_node --input-type=module - "$REA_CHECKOUT" <<'JS' import { pathToFileURL } from 'node:url'; const { readClientRegistrationStatuses } = await import(pathToFileURL( process.argv[2] + '/dist/application/ClientRegistrationStatus.js')); const status = (await readClientRegistrationStatuses(process.env.HOME, undefined, {environment:process.env, selectedClientIds:['opencode']})).find(s=>s.client==='opencode'); console.log(JSON.stringify({command:status.command,state:status.state})); JS } registration run_node "$REA_CHECKOUT/scripts/rea.mjs" uninstall --json > "$WORK/uninstall.json" echo "uninstall exit: $?" run_node -e 'const r=require(process.argv[1]);const o=r.items.find(i=>i.name==="opencode");console.log(JSON.stringify({status:r.status,opencode:o.status,actions:o.actions.map(a=>a.status)}))' "$WORK/uninstall.json" registration cat "$WORK/home/.config/opencode/opencode.json" "$WORK/home/.config/opencode/opencode.jsonc" ``` ## Expected behavior The documented uninstall rule says an unowned OpenCode V1 entry that removing V2 would expose stops preflight before any removal. These are supported user configuration layers. Uninstall should refuse this case before changing the native registration, as it already does when both rows occupy one file. ## Impact and scope The documented preflight protection against changing which registration is selected fails for this two-file layout. Uninstall reports success while REA's registration reader still resolves the retained V1 command. The lower file remains unchanged and the upper file has an empty native server table. Manually removing the unwanted lower V1 row before uninstall avoids the failure; the control without that row completed with registration `missing`. Native OpenCode startup and population frequency are NOT_ENOUGH_INFO; neither was measured. No purge-data consequence is claimed. ## REA version 6.8.0 source, baseline `ace4dbf36e8c8f36a0e192aff9a11484ed428358`, using the built checkout runtime. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1 arm64. No analysis provider or engine involved. ## Target and artifact context Two authored OpenCode user configuration files; no binary target or external artifact. ## Interface CLI ## Earliest failure stage Cleanup, cancellation, or lifecycle ## Relevant output or evidence The standalone reproduction printed: ```text {"command":["npx","-y","rea-agents@6.8.0","mcp"],"state":"aligned"} uninstall exit: 0 {"status":"complete","opencode":"removed","actions":["skipped","retained","removed"]} {"command":["/usr/bin/false"],"state":"stale"} {"mcp":{"rea":{"type":"local","command":["/usr/bin/false"],"enabled":true}}} {"mcp":{"servers":{}}} ``` - Expected source: `docs/installation.md:447-451` promises preflight refusal for an unowned V1 entry exposed by native removal; `docs/client-configuration.md:660-669` documents merged user configuration and uninstall across applicable files. - Failure source: the public CLI output and resulting files above, plus the executed production registration reader. Source tracing shows the preflight host inspects one candidate file at a time (`src/application/Uninstall.ts:461-550`); its fallback refusal considers the parsed document's legacy entry (`src/application/Uninstall.ts:774-826`). The effective registration reader instead merges applicable files (`src/application/ClientConfigurationFiles.ts:178-232`, consumed at `src/application/ClientRegistrationStatus.ts:183`). - Evidence provenance: observed for CLI status, file contents, and registration selection; static for the explanatory call path. - Local verification: reproduced, including execution of the standalone reproduction above. - Reproduction completeness: complete for REA's documented preflight behavior; native-client execution is outside the demonstrated result. ## Additional context Restoration check: with the same layered input, uninstall should report the V1 fallback refusal and leave both configuration files byte-identical. Retain the discriminating controls: both entries in one file already exit 1 with the fallback diagnostic and unchanged bytes; omitting the lower V1 row allows exit 0, removes the owned upper row, and leaves registration `missing` with command `[]`. Both controls were executed. Related history was read, including comments and closure explanations. [#4977](https://github.com/morluto/rea/pull/4977) was superseded by a main fix whose confirmed fallback controls concern entries in one document; that case passes here while the cross-file case fails. [#5079](https://github.com/morluto/rea/issues/5079) fixed lost reporting after a later file fails; this run preserves its per-file actions and has no removal failure. [#864](https://github.com/morluto/rea/pull/864) introduced V2/V1 handling, and [#6587](https://github.com/morluto/rea/pull/6587) concerns inherited native setup dialect. No matching open cross-file preflight report was found. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A supported two-file configuration defeats a documented uninstall preflight refusal, removes the owned native row, and changes REA's selected registration while reporting success. The observed consequence is bounded and reversible; native execution was not tested. - Workaround status: available - Workaround basis: Remove the unwanted lower V1 row before uninstall. The executed control without that row completed removal and returned registration missing with no command.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4349
No description provided.