Skip to content

fix(launcher): install formae only when the machine has none - #33

Open
JeroenSoeters wants to merge 6 commits into
mcp-hosted-routingfrom
mcp-one-install
Open

fix(launcher): install formae only when the machine has none#33
JeroenSoeters wants to merge 6 commits into
mcp-hosted-routingfrom
mcp-one-install

Conversation

@JeroenSoeters

Copy link
Copy Markdown
Contributor

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.sh ran provision_pkg formae on every launch, unconditionally —
not "only when none is detected". Meanwhile formaebin.Resolve() preferred
PATH. So a machine with its own formae ended up with two installs and used the
one we cannot upgrade:

/opt/pel/bin/formae            root:root   formae version: 0.89.0   <- what the MCP ran
~/.formae-ai/opt/bin/formae    user        formae version: 0.89.0   <- what we downloaded

Two different builds reporting the same version. /formae:upgrade made 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 sourceable provision.sh) and
nowhere 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 one
into ~/.formae-ai/opt. It exports:

  • FORMAE_BIN — the absolute path to the one formae.
  • FORMAE_BIN_MANAGED1 only when that binary is the one we installed, and
    can therefore be upgraded without sudo.

internal/formaebin collapses to reading those two variables (knownClassic Locations, Exists and BundledPath are gone; a PATH fallback remains for
running 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 PATH because a harness launched from a
desktop session can have a minimal PATH that omits them.

Consequences

  • Never two installs. If you have a formae, it is the formae — nothing is
    downloaded and nothing shadows it.
  • We never move an install we did not create. The skew notice now says which
    upgrade applies: /formae:upgrade when the copy is ours, or the path plus a
    sudo warning when it is yours. /formae:upgrade reads that instead of probing
    the filesystem for a copy that may not be the one in use.
  • FORMAE_BIN is the testing seam. Preset it and the launcher honours it
    verbatim, 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/formae

Tests

test/resolve-formae.sh covers the six cases (own install on PATH, own install
off PATH, clean machine, existing managed copy, managed copy appearing on
PATH, preset FORMAE_BIN) and is wired into CI. It is mutation-checked: both
removing 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/bin cannot leak into the clean-machine cases.

Go tests pass under -race; golangci-lint clean.

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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant