feat(mcp): resolve connections through the formae CLI and refuse hosted - #32
Open
JeroenSoeters wants to merge 9 commits into
Open
feat(mcp): resolve connections through the formae CLI and refuse hosted#32JeroenSoeters wants to merge 9 commits into
JeroenSoeters wants to merge 9 commits into
Conversation
Six request construction sites collapse into one executor that owns the endpoint, headers, cancellation and error construction. Every client method now takes a context and handlers pass their own instead of discarding it. A structural test pins the criterion: no direct HTTP request construction outside the executor, so no method can be added without the headers the executor guarantees.
Adds the oracle: run the CLI, read its machine view on stdout, and decode the arm-tagged connection. Stdout and stderr are captured separately and drained concurrently, output is bounded and the child is killed rather than waited on when it exceeds the bound, and a non-zero exit reports the status rather than the subprocess bytes. Not yet wired in; the swap lands with the configuration change.
…tection Adds FeatureConnectionOracle at a 0.89.0 floor and a context-aware guard. Detection now runs in its own process group with a kill hook, because cancelling exec.CommandContext stops only the immediate child while CombinedOutput waits for EOF a grandchild can still hold open. The subprocess also runs outside the package mutex, so one hung formae cannot block every other gated call, and a cancelled probe is not cached.
The PKL text scraper is gone. Configuration now comes from one CLI evaluation per call, and the execution context carries a Classic | Hosted connection instead of loose url/port/mode fields. Binary selection stops being mode-dependent: there is one formae per machine and the bundled copy is a fallback for a machine with none, so Resolve loses its mode parameter and Mode disappears. Handlers take the binary from the resolved context, so one call makes one binary decision. A hosted profile is decoded, validated against the origin grammar, and then refused. This build cannot authenticate, and sending a routing header without a credential would replace a comprehensible 'unsupported' with a remote 401. The deprecated FORMAE_AGENT_URL and FORMAE_AGENT_PORT variables go with the scraper; the CLI is now the only configuration authority.
CHANGELOG entries for CLI-resolved configuration, the single request executor, and the removal of FORMAE_AGENT_URL/FORMAE_AGENT_PORT. The README's Configuration section documented the deleted env-var precedence and the old cli.api block, so it is rewritten around the connection sum type and the per-call profile argument.
`formae profile show [<name>]` takes the name as an argument. There is no --profile flag on it and none on the root command either, so the flag form would have failed to parse and broken every tool call that names a profile. The argv test now pins both the named and active forms.
…e call eval and extract are separate formae invocations. They resolved the global active pointer independently, so a pointer moved by the user's CLI or another session between them could aim the work at a different agent than the one the call resolved. Both now name the profile the context resolved. Both also ran detached from the tool call. They now run in their own process group bound to the invocation context, so a cancelled call stops the CLI and the plugin children holding its output pipe.
This was referenced Aug 14, 2026
…cp-hosted-routing
JeroenSoeters
marked this pull request as ready for review
August 28, 2026 22:15
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.
Slice 1 of hosted support: make the formae CLI the MCP's configuration
authority, carry the resolved connection as a sum type through one immutable
per-call context, funnel every agent request through a single executor, and
recognise a hosted profile well enough to refuse it clearly.
Stacked on
mcp-orbital-provisioning(#31), which is stacked on #30. Nothinghere reaches
mainbefore the 0.89.0 cut-over.What changes
internal/configstops parsing PKL text.formae profile show --output-consumer machine --output-schema jsonis the oracle; its arm-taggedconnection decodes into a
Classic | Hostedsum type. The scraper(
AgentEndpoint,parseCliAPI, and the deprecated env vars) is deleted.internal/execctxcarries aconfig.Connectioninstead of four looseurl/port/mode/installation fields.
internal/servergrows one request executor. All six construction sitescollapse into it, every client method takes a context, and handlers pass their
own instead of discarding it.
internal/formaebinloses itsModeparameter. There is one formae permachine and the bundled copy is a fallback for a machine with none, so binary
selection is neither per-call nor mode-dependent.
build cannot authenticate, and sending a routing header without a credential
would replace a comprehensible "unsupported" with a remote 401. Slice 2 turns
it on.
Why an executor, structurally
TestNoDirectHTTPConstructionfails the build on anyhttp.NewRequest,httpClient.Do,http.Get(orhttp.Post(outsiderequests.go. Five of thesix original paths carried a
Client-IDheader; a per-method audit is exactlyhow one method ends up without it once hosted adds a routing header.
Review
Three cross-model Codex passes. Findings and dispositions are in the review
ledger Document on PLA-428. Three correctness findings were fixed with
failing→passing regression tests:
profile showtakes its profile name positionally; the flag form wouldnot have parsed, breaking every call naming a profile.
evalandextractre-resolved the global active pointer instead of theprofile the call froze.
evalandextractran detached from the tool call; they now run in theirown process group bound to the invocation context.
Draft: blocked on an upstream fix, not a decision
A clean or unmigrated install could not reach any agent-backed tool.
profile showread the active pointer directly, so on a machine where formaehas never run it failed with
not initialized, and the deletedAgentEndpointused to fall back to
http://localhost:49684there. The MCP's own sequence(
--version, thenprofile show --output-consumer machine) is exactly the twoinvocations that do not bootstrap the store, so a plugin-provisioned formae
would never have been initialised by anything.
Fixed at the source in platform-engineering-labs/formae#647:
profile showwith no name now takes the config-load path (
store.Resolve()), so a cleaninstall bootstraps from the stub — which already wires localhost, reproducing
the old fallback as a real profile — and a legacy
formae.conf.pklmigrates.show <name>,listandcurrentstay pure reads.Staying a draft until #647 merges and a dev build past it is cut. Note the
stablechannel cannot satisfy the 0.89.0 gate either: the newest tag is0.89.0-dev.5and the latest stable is0.88.1, so nothing in this stackreaches
mainbefore the 0.89.0 release.