refactor(host): read every configuration value from an explicit host (#3794)
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 --headpassed (receiptrun-LwZ5BO),exactCommit=true. - Non-test
commandHostEnvironment()reads outside host-kit: 34 at basec966724bea→ 2 spawn-boundwhichCmdPATH 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.cwdstays optional andAgentDeviceClientgains nohost; the CLI and MCP surfaces pass their own captured host env intoformatCliOutput. - 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 differentAGENT_DEVICE_IOS_RUNNER_LEASE_DIReach resolve their own value and neither the process value, plus a read outside any entered runtime refusing withapple_runner_host_unbound(runner-lease-runtime-host.test.ts,runner-owner-state.test.ts); theweb-provider-missingrefusal 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.createInteractorbinds 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 thevega-adapter-host-unboundrefusal (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), andcreateHostDiagnostics(host).toolchainCheckreports 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.