refactor(host): read every configuration value from an explicit host (#3794)

Merged
TH
15h ago
+3190-1130

Summary

Every non-test configuration read on the converted routes now takes an explicit HostEnvironment instead of the ambient commandHostEnvironment(): 34 ambient reads outside host-kit command execution became required-host parameters, per-runtime state, or construction-bound ports. The boundary reads are two whichCmd PATH lookups — apple whichCommand and linux whichOnPath — which read the command-host scope (the runtime host plus the per-request env overlay) at the point of spawning a host command. They are part of the command-execution boundary that #3753's gate will allow; they are not configuration owners. One out-of-composition fallback remains: the Linux display decision for standalone leaf callers that name no host, reading the command host at call time with no memo. Composed routes never reach it — the Linux interactor always enters the provider bound to its own host. Vega has no ambient read at all any more: inventory, doctor and the interactor adapter share one resolver over the runtime host, and the un-bound scope default refuses.

Every platform package now derives display, tool, and home configuration from the host bound at construction of its local tool provider: the Linux interactor builds createLocalLinuxToolProvider({ host }) per instance and owns its display decision on that object (no module cache, no reset helper; input resolution is not memoized because the interactor itself lives one request); the Vega plugin builds createLocalVegaCliAdapter(host) per interactor, and inventory, doctor and the adapter all resolve the executable through ONE resolver (vega-executable.ts) over that runtime host's PATH and host-derived home — createDeviceInventoryHost derives its homeDirectory from host.env (hostHomeDirectoryFromEnv — HOME ?? USERPROFILE, never the process home; a host that names none yields homeDirectory: undefined, which turns off only the home-derived default paths — Android's ~/Android/Sdk root and Vega's default-install probe — never the whole host, so inventory still works for every family), createHostDiagnostics passes its host into vegaToolchainCheck, and the un-bound scope default refuses with vega-adapter-host-unbound instead of reading an ambient environment; the Apple runner reads its configuration through the per-runtime host slot entered by the daemon route. platform-web exports one policy for the provider of a web request, resolveWebProviderForHost(host) — request-scoped provider wins, else the agent-browser provider bound to the composing host — and the interactor, the network transport and the audio probe all resolve through it, so every reader of one web request agrees on one provider. The un-scoped fallback stays fail-closed (web-provider-missing).

Work is split into four commits by layer: contracts + shared test-kit, Apple, Linux/Vega/web/HarmonyOS, root composition.

Part of #3747. Part of #3794 — part 2 (one entered runtime store, collapsing the per-module runtimeStates aggregation) is the next PR.

Validation

  • Tested SHA c466c4ee0a: pnpm check:affected --run --head passed (receipt run-LwZ5BO), exactCommit=true.
  • Non-test commandHostEnvironment() reads outside host-kit: 34 at base c966724bea → 2 spawn-bound whichCmd PATH lookups + 1 memo-less out-of-composition display fallback (standalone Linux leaves) at head. For composed runtimes none of these is a configuration read: display, tool and home configuration is bound at provider construction from the runtime host.
  • Public contract kept additive for 0.21.x: DebugSymbolsOptions.cwd stays optional and AgentDeviceClient gains no host; the CLI and MCP surfaces pass their own captured host env into formatCliOutput.
  • New tests: two runtimes route the same macOS device by their own host (interactors.test.ts); the composed daemon route configures interactors from its host, not the process (daemon-composition.test.ts); two runner runtime states with different AGENT_DEVICE_IOS_RUNNER_LEASE_DIR each resolve their own value and neither the process value, plus a read outside any entered runtime refusing with apple_runner_host_unbound (runner-lease-runtime-host.test.ts, runner-owner-state.test.ts); the web-provider-missing refusal for every un-scoped web provider operation (provider-composition-fallback.test.ts); two interactors on different hosts keep their own display decision across A(X11) → B(Wayland) → A with an opposing process env (interactor-host-isolation.test.ts); two Vega hosts resolve the CLI executable from their own PATH and HOME, each adapter memoizes on its own instance, and neither reuses the other's answer (tool-provider-host-isolation.test.ts); vegaPlugin.createInteractor binds each caller's host so the returned interactor runs that host's executable, against a process command host naming a third location — removing the binding makes it fall through to the vega-adapter-host-unbound refusal (index.test.ts); createVegaInventory(createDeviceInventoryHost(host)) lists devices through a host whose HOME differs from the process HOME and names the only vega (platform-runtime-host.test.ts), and createHostDiagnostics(host).toolchainCheck reports the CLI found for the same host with no provider scope (platform-runtime-host-diagnostics.test.ts) — both go red when the host-derived home or the doctor host binding is reverted.

CI obligations

Full CI on this head; device lanes run there. The macOS helper lane and the native-backend refusal were run live on 17ac24fb79 — see the evidence comment; the Apple routes are unchanged since.

Unresolved risks

None known beyond the review.