Skip to content

feat(mcp): resolve connections through the formae CLI and refuse hosted - #32

Open
JeroenSoeters wants to merge 9 commits into
mcp-orbital-provisioningfrom
mcp-hosted-routing
Open

feat(mcp): resolve connections through the formae CLI and refuse hosted#32
JeroenSoeters wants to merge 9 commits into
mcp-orbital-provisioningfrom
mcp-hosted-routing

Conversation

@JeroenSoeters

@JeroenSoeters JeroenSoeters commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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. Nothing
here reaches main before the 0.89.0 cut-over.

What changes

  • internal/config stops parsing PKL text. formae profile show --output-consumer machine --output-schema json is the oracle; its arm-tagged
    connection decodes into a Classic | Hosted sum type. The scraper
    (AgentEndpoint, parseCliAPI, and the deprecated env vars) is deleted.
  • internal/execctx carries a config.Connection instead of four loose
    url/port/mode/installation fields.
  • internal/server grows one request executor. All six construction sites
    collapse into it, every client method takes a context, and handlers pass their
    own instead of discarding it.
  • internal/formaebin loses its Mode parameter. There is one formae per
    machine and the bundled copy is a fallback for a machine with none, so binary
    selection is neither per-call nor mode-dependent.
  • Hosted is decoded, validated against the origin grammar, and refused. This
    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

TestNoDirectHTTPConstruction fails the build on any http.NewRequest,
httpClient.Do, http.Get( or http.Post( outside requests.go. Five of the
six original paths carried a Client-ID header; a per-method audit is exactly
how 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 show takes its profile name positionally; the flag form would
    not have parsed, breaking every call naming a profile.
  • eval and extract re-resolved the global active pointer instead of the
    profile the call froze.
  • eval and extract ran detached from the tool call; they now run in their
    own 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 show read the active pointer directly, so on a machine where formae
has never run it failed with not initialized, and the deleted AgentEndpoint
used to fall back to http://localhost:49684 there. The MCP's own sequence
(--version, then profile show --output-consumer machine) is exactly the two
invocations 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 show
with no name now takes the config-load path (store.Resolve()), so a clean
install bootstraps from the stub — which already wires localhost, reproducing
the old fallback as a real profile — and a legacy formae.conf.pkl migrates.
show <name>, list and current stay pure reads.

Staying a draft until #647 merges and a dev build past it is cut. Note the
stable channel cannot satisfy the 0.89.0 gate either: the newest tag is
0.89.0-dev.5 and the latest stable is 0.88.1, so nothing in this stack
reaches main before the 0.89.0 release.

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.
@JeroenSoeters
JeroenSoeters marked this pull request as ready for review August 28, 2026 22:15
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