fix(launcher): install formae only when the machine has none - #33
Open
JeroenSoeters wants to merge 6 commits into
Open
fix(launcher): install formae only when the machine has none#33JeroenSoeters wants to merge 6 commits into
JeroenSoeters wants to merge 6 commits into
Conversation
start-mcp.sh provisioned formae on every launch, then formaebin preferred PATH — so a machine with its own formae got a second copy that was never used, and /formae:upgrade would upgrade that unused copy and report success while the MCP kept running the other one. Detection now lives in one place. resolve_formae finds the user's own install (PATH, then the known locations) and provisions only when there is none, then exports FORMAE_BIN and FORMAE_BIN_MANAGED. formaebin reads that answer instead of repeating the search in Go: two detectors in two languages can disagree, and that disagreement is how the second install happened. FORMAE_BIN is honoured when preset, which is how you point the plugin at a specific build, and it counts as the user's own so nothing moves it. The skew notice now names the upgrade that applies, and the upgrade skill reads it instead of probing the filesystem for a copy that may not be the one in use.
… too old Four defects the first adversarial review of this branch turned up. A formae below the MCP's floor fails resolution before any skew notice can be produced, and the upgrade skill had just been rewritten to read that notice — so it saw nothing and reported that there was nothing to upgrade, dead-ending precisely the users whose MCP was broken. Gate refusals now carry a typed error, and resolution turns one into a message naming the binary and whether the plugin installed it. The skill reads either shape. Ownership is now derived rather than inherited: a stale FORMAE_BIN_MANAGED in the environment could mark a hand-pointed binary as ours, and the upgrade would then move the managed copy while the MCP kept running the other one. The probe also missed ~/.local/bin and ~/bin, so a common user install was invisible to a desktop-launched harness with a reduced PATH and we would lay a second formae beside it. The shipped defaults are pinned by a test, since the cases inject their own list and would not have caught it. Finally the skew notice claimed an install that is not ours needs sudo, which it cannot know — FORMAE_BIN can point at a user-owned build under $HOME. It now names the path and says only that the plugin will not change it.
The harness could not exercise a first run of this branch. It mounted the host's formae profiles, so the container was never a machine where formae had not run, and it pulled the MCP from a release channel, so the scripts and skills came from the branch while the binary did not. DEV_BUILD=1 installs the Go toolchain and sets FORMAE_MCP_DEV, so the MCP is built from the plugin checkout. MOUNT_PROFILES=0 leaves the config dir empty. ISOLATE_CLAUDE=1 mounts a throwaway copy of ~/.claude, so installing a plugin in the container cannot disturb the host's own plugin set.
JeroenSoeters
marked this pull request as ready for review
August 28, 2026 22:15
resolve_formae compared a candidate's path against the managed path as strings, so any other spelling of the same binary read as the user's own install and set FORMAE_BIN_MANAGED=0. A symlink into ~/.local/bin, or a home directory that is itself a symlink, was enough: the plugin then disowned the copy it had provisioned and /formae:upgrade refused to touch it. Resolve both sides before the comparison, and keep the resolved path in FORMAE_BIN. The path there is the one whose directory goes on PATH, and formae needs the pkl beside it to read plugin manifests; a symlink's directory holds no pkl. canonical_path walks the links by hand because readlink -f and realpath are both absent from a stock macOS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #32. Makes "there is exactly one formae per machine" true, which the
design assumed and the shipped launcher did not honour.
The bug
start-mcp.shranprovision_pkg formaeon every launch, unconditionally —not "only when none is detected". Meanwhile
formaebin.Resolve()preferredPATH. So a machine with its own formae ended up with two installs and used theone we cannot upgrade:
Two different builds reporting the same version.
/formae:upgrademade it worse:its Step 2 checked the managed tree first, so it would upgrade the unused
copy, report success, and leave the MCP running the other one.
The fix: one detector
Detection now lives in
resolve_formae(in the sourceableprovision.sh) andnowhere else. It finds the user's own install —
PATH, then/opt/pel/bin, then/usr/local/bin— and uses it; only when there is none does it provision oneinto
~/.formae-ai/opt. It exports:FORMAE_BIN— the absolute path to the one formae.FORMAE_BIN_MANAGED—1only when that binary is the one we installed, andcan therefore be upgraded without sudo.
internal/formaebincollapses to reading those two variables (knownClassic Locations,ExistsandBundledPathare gone; aPATHfallback remains forrunning the binary directly). Go no longer repeats the search: two detectors in
two languages can disagree, and that disagreement is exactly how the second
install happened.
The known locations are probed after
PATHbecause a harness launched from adesktop session can have a minimal
PATHthat omits them.Consequences
downloaded and nothing shadows it.
upgrade applies:
/formae:upgradewhen the copy is ours, or the path plus asudo warning when it is yours.
/formae:upgradereads that instead of probingthe filesystem for a copy that may not be the one in use.
FORMAE_BINis the testing seam. Preset it and the launcher honours itverbatim, treating it as your own install so nothing upgrades it. Pair with
FORMAE_MCP_CHANNEL=dev:FORMAE_MCP_CHANNEL=dev FORMAE_BIN=~/.formae-ai/opt/bin/formaeTests
test/resolve-formae.shcovers the six cases (own install onPATH, own installoff
PATH, clean machine, existing managed copy, managed copy appearing onPATH, presetFORMAE_BIN) and is wired into CI. It is mutation-checked: bothremoving the managed-copy guard and restoring the unconditional provisioning make
it fail. The fixed locations go through a test seam so a machine that really has
formae at
/opt/pel/bincannot leak into the clean-machine cases.Go tests pass under
-race;golangci-lintclean.