Skip to content

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

Open
thymikee wants to merge 4 commits into
mainfrom
refactor/explicit-host-config
Open

thymikee wants to merge 4 commits into
mainfrom
refactor/explicit-host-config

Conversation

@thymikee

@thymikee thymikee commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

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. Two out-of-composition fallbacks remain, each reading the command host at call time with no module-level memo, and each unreachable from a composed runtime because that runtime's interactor always enters the provider bound to its own host: the Linux display decision for standalone leaf callers, and the Vega executable lookup for the host-diagnostics route.

Every platform package now derives display, tool, and home configuration from the host bound at construction of its local tool provider, and every memo lives on that owner: the Linux interactor builds createLocalLinuxToolProvider({ host }) per instance and owns its display decision and input-tool selection on that object (no module cache, no reset helper); the Vega plugin builds createLocalVegaCliAdapter(host) per interactor and resolves the executable from that host's PATH and HOME with an instance memo (host.env.HOME, not os.homedir()); 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 22ce5974cd: pnpm check:affected --run --head passed (receipt run-i6wvDF), exactCommit=true.
  • Non-test commandHostEnvironment() reads outside host-kit: 34 at base c966724bea → 2 spawn-bound whichCmd PATH lookups + 2 memo-less out-of-composition fallbacks (Linux display for standalone leaves, Vega executable for host diagnostics) 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 and input-tool selection across A(X11) → B(Wayland) → A with an opposing process env and no cache resets (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).

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.

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.41 MB 5.40 MB -8.0 kB
Package (unpacked) 5.41 MB 5.40 MB -8.0 kB
Package (download) 1.63 MB 1.63 MB -2.4 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 41.1 ms 41.2 ms +0.2 ms
CLI --help 80.2 ms 79.1 ms -1.1 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed 7c10533. The refactor looks sound, but the macOS helper route has no live run, and typecheck fails against current main.

Typecheck fails with TS2741 because of a real semantic conflict. This PR makes env required on runMacOsScreenshotAction options (helper.ts:607-612). Main's feca8cb (#3761) adds a call in helper.test.ts that passes only {surface: nativeApp, bundleId}. Please rebase onto 11de319 and pass env in that call. The Coverage check failure is unclear from the log excerpt I have, so please re-check it after the rebase. The Smoke failure is in the Swift RunnerTests (testPlanMovesPastATreeTierWhoseCollapsedTabQueryCannotStart, a tree_processing main-thread timeout). This PR changes only .ts files, so that failure is likely unrelated.

The macOS helper and native-app routes changed, but the PR's live run covers only the iOS simulator runner route. The env is now threaded into runMacOsHelper, the ghost-cursor and helper-path overrides, and the native-app backend choice. That choice now reads AGENT_DEVICE_MACOS_APP_BACKEND from the runtime host (interactor.ts:67, runtime.ts:268). If the helper-path or backend env is missing or wrong on that route, a helper-routed snapshot or press breaks on real macOS. helper.test.ts would not catch it because it stubs the helper provider. Please run a macOS daemon session on the head SHA: open <bundle> --platform macos with a helper-routed surface (app or frontmost-app), then snapshot and press. The output must show helper nodes. Then start the daemon with AGENT_DEVICE_MACOS_APP_BACKEND=native while the shell env differs, and show back refused with reason unsupported-device-backend. That proves the backend is read from the runtime host.

Not blocking, and you can take or leave these: (1) the Apple runner host slot (runner-host.ts:78) feeds about 20 runner config reads, yet every test binds it to the live env or process.env, so swapping the slot read for captureHostEnvironment(process) leaves the suite green and nothing reaches apple_runner_host_unbound. A test that composes two runner runtime states with different AGENT_DEVICE_IOS_RUNNER_LEASE_DIR values, and checks that each resolves its own and neither the process value, would cover it, plus one read outside any entered state expecting details.reason === 'apple_runner_host_unbound'; (2) unscoped web requests now follow two policies: createWebInteractor (interactor.ts:20) builds a host-bound provider, while the network transport and audio-probe resolvers get uncomposedWebProvider with no dumpNetwork or probeAudio, and nothing tests the web-provider-missing refusal, so every reader in one unscoped web request should resolve the same provider; (3) DebugSymbolsOptions.cwd (debug-symbols.ts:8) goes from optional to required and AgentDeviceClient gains a required public host, both public since v0.21.28 and without a CHANGELOG entry, so either keep cwd optional and take the formatter env from the CLI or MCP surface's own host, or document both; (4) the PR says "Closes #3794" but part 2 (the runtimeStates aggregation) remains and is not mentioned, so "Part of #3794" with part 2 noted as deferred would be accurate; (5) the exitWithHelp extraction (cli.ts:304) and the removed fallow-ignore comments in client-types.ts are unrelated, and the PR is one 219-file commit of about 3,900 gross lines, so splitting by layer (contracts and plugin port, Apple, Linux/Vega/web, root composition) would ease review.

Could the env be bound when the local Linux and Apple tool providers are built (for example createLocalLinuxToolProvider(host) inside createLinuxInteractor(host)) instead of being added to every port method and helper? Could runCliOnHost and the MCP executor deps pass their own host env into formatCliOutput, so the public AgentDeviceClient.host is not needed? Could one Apple runner runtime state be entered at each process entry that already calls withCommandHost (src/cli.ts:140 and the two scripts), instead of setters in runStaleDeviceClaimRelease and createDaemonOwnerCleanup? With DebugSymbolsOptions left unchanged, that would remove the four setter sites and most positional env parameters. What would need to change first is part 2 of #3794: one entered runtime store where slots take the runtime's host as their initial value, so the runner host needs no wrapper or setters.

I did not review every Linux, Vega and web leaf for a missed host thread, though the required parameter types turn a miss into a compile error. Some ambient process.env configuration reads remain outside this change (for example src/platform-runtime-android-adb-host.ts:30), so "every configuration value" in the title overstates the scope a little.

Before merge, please rebase onto 11de319 and pass env in the #3761 helper.test.ts call. Then add the macOS helper-surface live run.

@thymikee
thymikee force-pushed the refactor/explicit-host-config branch from 7c10533 to 17ac24f Compare October 18, 2026 15:34
@thymikee

Copy link
Copy Markdown
Member Author

All items from the 12:41Z review applied. Rebased onto 11de319, restructured into four layer commits, force-pushed as 17ac24fb79.

Blocking

  1. Typecheck vs main — done: rebased onto 11de319aad, and the fix(apple): refuse captures whose window or panel identity is unknown #3761 runMacOsScreenshotAction call in helper.test.ts now passes env. pnpm typecheck passes.

  2. Live macOS evidence on this head — both runs below are on 17ac24fb79 (local build + pnpm build:macos-helper), macOS host INV-0357-Michal-Pierzchala, separate state dirs.

    a. Helper-routed surface (frontmost-app → macos-helper backend per macOsSurfaceBackend), then snapshot and press — helper nodes visible:

    $ agent-device open --platform macos --surface frontmost-app --state-dir /private/tmp/ad-3798-a
    Opened: Finder
    
    $ agent-device snapshot --platform macos --state-dir /private/tmp/ad-3798-a
    Page: Finder
    App: com.apple.finder
    Snapshot: 1496 nodes (truncated)
    @e1 [application] "Finder"
      @e2 [window] "Applications"
        @e3 [axsplitgroup]
          @e4 [scroll-area] "_NS:61" [scrollable]
            @e5 [axoutline] "sidebar"
              @e6 [axoutlinerow]
                @e7 [axcell]
                  @e8 [text] "Recents"
    
    $ agent-device press @e8~s539313 --platform macos --state-dir /private/tmp/ad-3798-a
    Tapped @e8 (384, 215)

    The AX-role node shape (axoutline/axoutlinerow/axcell, _NS: identifiers) is the helper traversal's output — the xctest runner route does not emit these. The press resolved a helper ref and acted through the helper's AX path.

    b. Backend read from the runtime host, not the shell: daemon started with AGENT_DEVICE_MACOS_APP_BACKEND=native in its own env; the back command ran from a shell where that variable is unset, and the daemon's runtime-host value still governed the route:

    $ AGENT_DEVICE_MACOS_APP_BACKEND=native agent-device open --platform macos --state-dir /private/tmp/ad-3798-b
    Opened: default
    
    $ env | grep MACOS_APP_BACKEND        # (nothing: shell env differs from the daemon's)
    $ agent-device back --platform macos --state-dir /private/tmp/ad-3798-b --json
    {
      "success": false,
      "error": {
        "code": "UNSUPPORTED_OPERATION",
        "message": "back is not supported on this device",
        "hint": "The native macOS app backend never starts the XCTest runner, which this command needs. Unset AGENT_DEVICE_MACOS_APP_BACKEND to use XCTest.",
        "details": { "reason": "unsupported-device-backend", "dispatched": "no" }
      }
    }

    The refusal reason comes from the daemon's runtime-host read of AGENT_DEVICE_MACOS_APP_BACKEND (runtime.ts macOsAppBackend → host.hostEnvironment.env), proving the native-app backend choice is host-threaded end to end. Sessions closed and both daemons stopped after the runs.

Contract (0.21.x additive)

  1. DebugSymbolsOptions.cwd is optional again; AgentDeviceClient no longer exposes host (the field and the type import are removed from the published surface). The client resolves cwd from its own host/config as before. runCliCommand and the MCP tool executor capture their own host per call and pass its env into formatCliOutput — no public host surface.

Simplify

  1. Local tool providers no longer take env through their port methods: LinuxToolProvider.whichCommand(cmd), LinuxDesktopProvider.openTarget/closeApp, LinuxClipboardProvider, LinuxScreenshotProvider.capture, the Apple whichCommand port, and the Vega provider all lost the env parameter. What remains ambient in those providers is five commandHostEnvironment() reads at the point of spawning a host command (apple whichCommand, linux whichOnPath/isLinuxWayland/detectDisplayServer, vega resolveVegaCliExecutable). Per the coordinator ruling these stay: they read the command-host scope (runtime host plus the per-request overlay) exactly where host-kit materializes a spawn, so they are the command-execution boundary chore(layering): forbid process globals outside entry points #3753's gate will allow, not configuration owners. Named as such in the PR body.
  2. One policy per unscoped web request: resolveWebNetworkTransport and the audio-probe resolver now build the same host-bound createAgentBrowserWebProvider({ host }) that createWebInteractor builds when no provider is scoped; only an actual provider-less scope inside a request keeps the fail-closed uncomposedWebProvider. The web-provider-missing refusal is now tested for every provider operation.

Tests and hygiene

  1. runner-lease-runtime-host.test.ts: two runner runtime states with different AGENT_DEVICE_IOS_RUNNER_LEASE_DIR — A's lease resolves only inside A, B and the process default resolve neither. runner-owner-state.test.ts: a read outside any entered runtime refuses with details.reason === 'apple_runner_host_unbound'.
  2. PR body says "Part of refactor(host): explicit host for configuration reads and one entered runtime store #3794" and names part 2 (one entered runtime store) as the next PR.
  3. exitWithHelp is back inline in cli.ts; the client-types.ts fallow-ignore comments are restored unchanged. The work is now four commits by layer: contracts + shared test-kit, Apple, Linux/Vega/web/HarmonyOS, root composition.

Also noted

  • The Smoke Swift failure (testPlanMovesPastATreeTierWhoseCollapsedTabQueryCannotStart) is the known intermittent runner-timing test; that job is being re-run for this head.
  • On the title overstatement: agreed — the Android adb-host reads and other ambient process.env lookups are outside this change; chore(layering): forbid process globals outside entry points #3753's gate is what bounds the remaining surface.

Gate: pnpm check:affected --run --head 17ac24fb79 passed — all runnable checks, receipt run-PYGQAj, exactCommit=true.

@thymikee
thymikee force-pushed the refactor/explicit-host-config branch from 17ac24f to 0968cb7 Compare October 18, 2026 15:48
@thymikee

Copy link
Copy Markdown
Member Author

Coverage failure on 17ac24fb79 fixed and pushed as 0968cb7343 (same four layer commits, one amended).

The job was not a coverage gap: the unit-ci lane's scripts/__tests__/test-file-size-ratchet.test.ts guard failed — src/mcp/__tests__/command-tools.test.ts measured 1047 lines vs 1029 at the merge-base. The growth came from this round's review fixes: ~30 MCP createClient stubs were widened with a fake host ({ host: { env: process.env, cwd: process.cwd() } }) across four MCP test files. That was unnecessary — after the item-3 change nothing reads client.host; the executor captures its own host per call and passes host.env into formatCliOutput directly. The four files are restored to their exact main versions; the executor still receives its host from captureHostEnvironment(process).

  • Focused MCP tests pass (4 files, 66 tests), as does the ratchet guard and pnpm typecheck.
  • pnpm check:affected --run --head 0968cb7343: all runnable checks passed, receipt run-XwnP22, exactCommit=true.

Live macOS evidence and the item-by-item replies above are unchanged by this push (the diff only reverts MCP test-file stubs).

@thymikee

Copy link
Copy Markdown
Member Author

One remaining issue at 0968cb7343: the earlier coordinator exception for all five ambient reads was too broad.

P1 — Linux host isolation: packages/platform-linux/src/linux-env.ts:15-38 chooses X11/Wayland via commandHostEnvironment() and caches the chosen input tool solely by provider identity. tool-provider.ts:305-314 independently repeats the display decision. These are configuration decisions, not host-kit spawn materialization. With two host runtimes using the default local provider, run an X11 input first, then a Wayland input: both resolve the same singleton provider, so the second ensureInputTool() returns the first runtime's cached xdotool selection before reading its own host. The current isolation test constructs distinct providers and resets the cache, so it does not cover this route.

Please bind display configuration to the runtime host/provider at construction and scope the input-tool cache to that owner (or remove the cache if it is unnecessary). Reuse one display detector for input, clipboard and screenshot selection. Keep env out of individual port methods. Add an A(X11) → B(Wayland) → A test with independent explicit hosts and an opposing process env, without cache resets between calls. The gate in #3753 must not exempt these configuration reads as command execution.

This corrects the coordinator's earlier ruling; the PR's macOS evidence and exact-head validation receipt remain useful. I have inspected this path read-only and have not run a Linux device session.

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed 0968cb7. Two Linux and Vega configuration reads still depend on process-wide stored state, so the host is not the only source of configuration yet. The CI checks are green: 17 checks, 0 failing at 0968cb7. The earlier Coverage failure (the test-file-size limit on command-tools.test.ts) is fixed by the MCP stub revert. Before merge, the Linux and Vega tool providers need to be bound to their host.

The first problem is in linux-env.ts. The Linux plugin's createInteractor drops the host it receives (register-builtins.ts:35). ensureInputTool then keeps {tool, display} in a module-global variable keyed only by provider identity, and without a scoped provider every runtime shares one localLinuxToolProvider. Take two runtimes in one process: A has an X11 host, B has WAYLAND_DISPLAY set. B's first press gets A's stored xdotool before B's host is read. Clipboard and screenshot make a separate display decision (isLinuxWayland, tool-provider.ts:305). So in B, input uses xdotool while screenshot uses grim, and input can silently do nothing. The code is unchanged from main, so this is not a regression. But the PR body calls these reads the command-execution boundary, and they are not, so #3794 is not done for Linux. The rule: every platform package derives display, tool and home configuration from the host bound when its local tool provider is built, and any memo lives on that owner, not in a module global. Please build the provider inside createLinuxInteractor(host), compute the display once on it for input, clipboard and screenshot, and delete the input-tool reset helper. A regression test should run A(X11), then B(Wayland), then A through the plugin's createInteractor route, with explicit hosts, an opposing process env and no reset between runs. It should fail on the current code.

The second problem is the same rule in the Vega tool provider. resolveVegaCliExecutable stores the result in an unkeyed module global, so the first runtime's PATH lookup answers for all later runtimes. The fallback at line 101 uses hostHomeDirectory(), which is os.homedir(), not the host's HOME. A second runtime with a different PATH or HOME can run the first runtime's vega binary, or miss its own. The Vega plugin already receives the host at index.ts:49, so please bind it into the provider there and resolve the executable from host PATH and HOME on that instance.

Not blocking, and you can take or leave these: the "scoped ? resolveWebProvider() : createAgentBrowserWebProvider({host})" rule appears three times (platform-web interactor.ts:20, network-web-transport.ts:14 and audio-probe-host.ts:68), so one resolveWebProviderForHost(host) export in platform-web would own it. Also, once the two fixes land, the PR body should list only the two whichCmd PATH lookups as boundary reads, and the commit dcece50 title names Linux and Vega, which this head does not change.

Could the host the plugin port already delivers be enough here? If createLinuxInteractor(host) builds the provider, with the display computed once and the input memo on that instance, the module globals and the isLinuxWayland duplicate go away. The port methods stay free of env parameters, and the ambient reads shrink to the two whichCmd lookups. Nothing needs to change first, because the host argument already reaches register-builtins.ts:35.

On evidence: the live macOS run was on 17ac24f, which I could not find locally, so I did not confirm that the range to 0968cb7 touches only MCP test files. The native-backend live run starts the daemon with its own process env, so it cannot tell the runtime host from the process env. The unit daemon-composition test carries that proof, and I did not re-run it. I did not run a two-runtime Linux or Vega session; both findings come from reading the code, and the memo key and singleton resolution are confirmed by grep. The macOS evidence the PR needs is in place. No conflicts are known.

@thymikee

Copy link
Copy Markdown
Member Author

Coordinator note for the #3794 owner. The 16:35Z review is only partly covered by the local round I see (613e66bdf5, not pushed yet). Before the next push, or in the round right after it:

  1. Linux: keep the input-tool selection on the provider instance that createLinuxInteractor(host) builds, not in the module-level cachedInputTools WeakMap. Delete the reset helper (linux-env.ts:65).
  2. Vega: resolveVegaCliExecutable still caches its result in an unkeyed module global. tool-provider.ts:101 still calls hostHomeDirectory(). Bind the host into the provider at platform-vega/src/index.ts:49, then resolve the executable from the host's PATH and HOME on that instance. Add a test with two hosts.
  3. Web (recommended, apply): add one resolveWebProviderForHost(host) export in platform-web. Replace the three copies of the scoped-or-agent-browser rule with it.
  4. PR body and commits (recommended, apply): the PR body lists only the two whichCmd PATH lookups as boundary reads. Each commit title names what that commit changes.

When done, post one comment that maps each item to its commit.

@thymikee
thymikee force-pushed the refactor/explicit-host-config branch from 0968cb7 to 613e66b Compare October 18, 2026 17:25
@thymikee

Copy link
Copy Markdown
Member Author

Linux host-isolation P1 from the coordinator review fixed and pushed as 613e66bdf5 (same four layer commits, Linux and root-composition commits amended).

What changed:

  • One display detector, bound at construction. New packages/platform-linux/src/display.ts owns the DisplayServer vocabulary, detectLinuxDisplayServer(env), and LinuxDisplayConfig — a display decision closed over one HostEnvironment. createLocalLinuxToolProvider({ host, ... }) now requires the host and binds the config into the provider; input, clipboard and screenshot selection all read that one bound answer. No port method gained an env parameter.
  • The ambient reads are gone. linux-env.ts detectDisplayServer and tool-provider.ts isLinuxWayland no longer call commandHostEnvironment(). Linux ambient reads outside spawn materialization: whichOnPath only (the per-spawn which lookup, which the R13 substrate scan accepts alongside runCmd). The platform package now reads no process and no ambient host for configuration — the layering guard enforces exactly that (R13 flagged my first draft for a process fallback here, so the out-of-scope fallback provider reads the command host at call time instead, reachable only by standalone leaf callers, never by a composed runtime, which always binds its host-bound provider).
  • The cache is per provider. ensureInputTool caches in a WeakMap keyed by the resolved provider object, so the cache belongs to the host-bound provider that made the decision and dies with it. The composing route: createLinuxInteractor(host) resolves hostBoundLinuxToolProvider(host) (one provider per host object) and enters it per interactor method unless the request scopes a provider, so a scoped test/provider override still wins.
  • The test you asked for: packages/platform-linux/src/__tests__/interactor-host-isolation.test.ts drives two createLinuxInteractor interactors over the plain (non-overridden) provider surface with independent explicit hosts — A(X11) → B(Wayland) → A resolves xdotool → ydotool → xdotool, with the process environment claiming a third Wayland display that neither host names, and no cache resets between the calls. On the old singleton-keyed cache, phase B answered A's cached xdotool. The old scope test remains (now with bound hosts) and resetInputToolCache only clears the fallback provider's entry.

Validation: pnpm check:affected --run --head 613e66b: receipt run-W7ZqY7, exactCommit=true; platform-linux suite 55/55; typecheck and layering guard pass. No allowlists added anywhere.

@thymikee
thymikee force-pushed the refactor/explicit-host-config branch from 613e66b to 5b1d1a5 Compare October 18, 2026 18:32
@thymikee

Copy link
Copy Markdown
Member Author

Round 3 — the 16:35Z review items and the four remaining items from the 17:23Z note, each mapped to its commit at head 5b1d1a5213:

1. Linux input-tool memo on the provider instance; reset helper dropped — 75e05d46fa.
The cachedInputTools WeakMap and resetInputToolCache are deleted. ensureInputTool now memoizes on the resolved provider itself (provider.inputSelection, tagged with the display decision it resolved for; a refusal is never cached and a display change re-resolves). createLinuxInteractor(host) builds its own createLocalLinuxToolProvider({ host }) per interactor — no host-keyed module map, no reset hook — and a request-scoped provider still wins. input-actions.test.ts, tool-provider.test.ts and interactor-host-isolation.test.ts no longer reset anything between tests; the isolation test passes A(X11) → B(Wayland) → A with a fresh interactor per phase and an opposing process env.

2. Vega host bound at the plugin route — 75e05d46fa.
vegaPlugin.createInteractor(device, runner, host) builds createLocalVegaCliAdapter(host) per interactor and enters it around every method unless the request already scopes a provider. The adapter resolves the executable from host.env PATH (whichCmd('vega', host.env)) and the default install from host.env.HOME/USERPROFILE — os.homedir() is gone — and memoizes the found executable in its own closure; misses are not memoized. The module global cachedVegaCliExecutable is deleted; the un-composed fallback adapter answers from the command host at call time with no memo, reachable only outside composed runtimes (host diagnostics), like the Linux display fallback. New tool-provider-host-isolation.test.ts: host A carries vega on PATH, host B only under its own HOME default install (real executable files), the process PATH/HOME name a third location with nothing; B resolves its absolute default-install binary, A resolves its PATH entry, neither reuses the other's answer, and later calls never re-probe — the shared module cache at the previous head failed this test.

3. One web provider policy — resolver added in 75e05d46fa, call sites replaced in 5b1d1a5213.
platform-web exports resolveWebProviderForHost(host, provider?): explicit provider wins, else the request-scoped provider, else the agent-browser provider bound to the composing host. The web interactor, the network transport and the audio probe all resolve through it; the transport keeps its own hasScopedWebProvider() read only to report mode: 'transport-composed' | 'local', not to pick a provider.

4. PR body + commit titles — body rewritten at this push. Boundary reads are now exactly the two whichCmd PATH lookups (apple whichCommand, linux whichOnPath); the two remaining out-of-composition fallbacks (Linux display for standalone leaves, Vega executable for host diagnostics) are stated separately as memo-less command-host reads unreachable from composed runtimes. With items 1–2 landed, the Linux/Vega/web/HarmonyOS commit title now names only packages that commit changes.

Two dead-code removals the fallow gate flagged on this round's diff: the DisplayServer/InputTool re-exports on linux-env.ts (75e05d46fa; consumers read the types from display.ts/input-tool.ts directly), and src/__tests__/daemon-composition/platform-services-fixture.ts (5b1d1a5213), which had no consumer at base or head — #3544 moved that fixture package-local and left this copy behind; the package-local copy's comment now names src/daemon-composition.ts as the composition lane's real-port owner.

Evidence notes: on the live-run observation — agreed, the native-backend refusal run starts the daemon from its own process env, so it cannot separate runtime host from process env there; the composed-route proof stays the daemon-composition.test.ts unit. On the ratchet — the earlier Coverage failure was the test-file size ratchet on src/mcp/__tests__/command-tools.test.ts after item 3 restored four MCP test files to their main versions; green since.

pnpm check:affected --run --head 5b1d1a5213 passed with exactCommit=true receipt run-Oq5N07. CI runs the full lanes on this head.

@thymikee
thymikee force-pushed the refactor/explicit-host-config branch from 5b1d1a5 to 22ce597 Compare October 18, 2026 18:53
@thymikee

Copy link
Copy Markdown
Member Author

Coverage failure on 5b1d1a5213 fixed at head 22ce5974cd (same tree except this fix; squashed into the Linux/Vega/web commit, check:affected --run --head 22ce5974cd passed, receipt run-i6wvDF, exactCommit=true).

The Coverage gate flagged resolveVegaCliExecutable at CRAP 30 (cyclomatic 5, 0% coverage): after the round-3 rework, only the out-of-composition fallback adapter called that helper, and no test route reaches the fallback — the two-host test drove the adapter's own inlined copy. The fix deduplicates rather than adds a parallel test: createLocalVegaCliAdapter now keeps only the instance memo and delegates the lookup to the shared resolveVegaCliExecutable, so one function owns PATH-then-HOME resolution and both adapters share it. The two-host test now measures that shared helper through the bound adapter (PATH hit, HOME default-install hit), and a new case covers the miss routes: a host with no HOME and a host whose HOME carries no default install both report unavailable, and a later call re-probes instead of trusting a memoized refusal.

This branch has not been deployed

No deployments
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