Skip to content

feat(host): capture HostEnvironment once at entry points - #3763

Open
thymikee wants to merge 9 commits into
mainfrom
refactor/host-environment
Open

thymikee wants to merge 9 commits into
mainfrom
refactor/host-environment

Conversation

@thymikee

@thymikee thymikee commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Part of #3747. Closes #3749.

HostEnvironment = { env, cwd } now has one owner. Entry points capture it once, and the daemon runtime, request scope and command spawning receive it instead of reading process. About 260 files change, most of them test call sites.

  • Type and capture: HostEnvironment is in @agent-device/kernel/source-value. captureHostEnvironment(process) is in @agent-device/host-kit/process. It copies the environment.
  • Entry points: src/bin.ts, src/cli.ts (runCli(argv, deps, host)), src/daemon.ts and createAgentDeviceClient(config, { host }) capture once. A client built without a host captures process and then reads only that host: its default transport, daemon launch, state dir and stateDir() all resolve from it.
  • Daemon: startDaemonRuntime requires host. DaemonRuntimeDepsInput.host replaces env. RequestRouterDeps.host and RequestExecutionScope.host carry it.
  • Command spawning: withCommandHost({ host, requestEnv }, fn) replaces withRequestCommandEnv. Spawned commands start from host.env. commandHostEnvironment() gives platform code that host and throws outside one.
  • Explicit inputs: whichCmd, resolveUserPath, expandSessionPath, the device-claim functions, loadDaemonPolicy, launchDaemonProcess and kernel allowlist and simulator-set resolvers take env or cwd as parameters.
  • Done when: host-kit, kernel and daemon-contracts non-test sources have no process.env or process.cwd().

Behavior changes: a relative state dir without a cwd is refused (relative_state_dir_without_cwd; callers now pass one), and a relative AGENT_DEVICE_CLAIMS_DIR is refused (relative_claims_dir; the store is shared by every daemon on the host, so it used to depend on where one of them started). Suites that reach host-reading code outside a daemon request or the CLI bind a host with bindTestCommandHost() from test-kit; there is no global test setup.

Non-owned packages keep their direct reads as inline process.env / process.cwd() for #3750 and #3751 to migrate.

Validation

pnpm check:affected --run --head 756cf4e012 (exactCommit) passed. GitHub-authoritative lanes (coverage, integration, native) are not run locally. No device-facing behavior changed.

🤖 Generated with Claude Code

thymikee and others added 3 commits October 16, 2026 22:03
Add HostEnvironment to the kernel and captureHostEnvironment to host-kit.
The CLI, the daemon executable and the client factory capture it once;
startDaemonRuntime requires it and the request scope exposes it. Host-kit,
kernel and daemon-contracts no longer read the process environment or
working directory.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…irectory

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Platform packages may not read process; they take the environment and
working directory of the enclosing command host. Unit tests enter the live
process environment as their host.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.32 MB 5.33 MB +3.0 kB
Package (unpacked) 5.32 MB 5.32 MB +3.0 kB
Package (download) 1.60 MB 1.60 MB +1.3 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.7 ms 40.2 ms +14.5 ms
CLI --help 71.0 ms 74.5 ms +3.5 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 178 files

You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/commands/connection-runtime.ts Outdated
Comment thread packages/daemon/src/sessions/handlers/session-boot.ts Outdated
Comment thread packages/daemon/src/server/http/daemon-shutdown-claims.ts Outdated
Comment thread packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts Outdated
Comment thread src/cli/commands/device.ts Outdated
Comment thread packages/daemon/src/server/__tests__/request-execution-scope.test.ts Outdated
Comment thread src/daemon-entrypoint.test.ts
Comment thread scripts/print-daemon-state-dir.ts Outdated
Comment thread packages/host-kit/src/command.ts Outdated
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 39 files (changes from recent commits).

You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/commands/connection.ts
Comment thread src/cli/commands/connection.ts
Comment thread packages/platform-android/src/app-deployment.ts
Comment thread packages/daemon-contracts/src/device-claim-paths.test.ts
…CLI env plumbing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

The code needs no changes before merge, but the kernel-errors mutation checks are red and must pass first. I reviewed commit 5effcd7.

All five kernel-errors shards fail the Stryker dry run on one test, host-kit/src/code-signature.test.ts:149 ("the daemon source graph stamps the walker that produced it"). That test is unchanged and passes in the unit lanes. The shards passed in run 38965344815. The related-test set for these shards grew from 1372 to 1607 files, which pulled this test in. The new global setup file (https://github.com/callstack/agent-device/blob/5effcd7/src/__tests__/host-environment-setup.ts#L8) imports host-kit/command and so exec.ts and kernel/errors, so I think it caused that growth. I did not reproduce the failure in the sandbox. The walker probably resolves workspace symlinks outside the sandbox root, but I have not proven that. The "Affected decision-kernel mutants" check fails after the dry run ("13 report(s), expected 18"), and Smoke Tests was still running when I looked. No conflicts.

The next step is to remove the global aroundEach setup, or to make that test work in the sandbox. Re-run the kernel-errors shards after that.

Not blocking, and you can take or leave these. The setup file binds every unit test to the live process.env and process.cwd(), so no test can see host_environment_unscoped (exec.ts:140). Please bind createTestHostEnvironment only in suites that enter through the daemon or CLI, and add one test that calls a commandHostEnvironment() consumer outside a scope and expects that reason. Also, createAgentDeviceClient(config, { host }) at https://github.com/callstack/agent-device/blob/5effcd7/src/agent-device-client.ts#L89 uses host only in stateDir(), while the daemon client still reads process.env and process.cwd() (daemon-client-lifecycle.ts:97-140, 818). Either every resolution the client makes should read the same host, or deps.host can wait for #3751. Finally, RequestExecutionScope.host (https://github.com/callstack/agent-device/blob/5effcd7/packages/daemon/src/server/request-execution-scope.ts#L263) has no production reader except its own test, because daemon handlers use the ambient commandHostEnvironment() (19 call sites).

Would one channel per layer be simpler? host-kit would keep the ALS scope only for spawn env and the DEVELOPER_DIR overlay. Daemon handlers would read host from RequestExecutionScope or the runtime deps they already get. Platform code would take host from its existing host or tool-provider object, as AndroidAdbHost.environment does. That would remove the throwing ambient accessor and the test-wide shim, and a missing host would become a type error. #3750, #3751 and #3752 will copy whatever shape this PR sets. Could we settle in #3747 or an ADR whether HostEnvironment travels explicitly or ambiently before they land?

The open inline threads still apply. The two P2 threads on connection.ts (#3763 (comment) on the home path, #3763 (comment) on provider profile) and the P2 thread on bundletool lookup (#3763 (comment)) are still open. One lower-priority thread also applies: #3763 (comment).

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files (changes from recent commits).

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/__tests__/host-environment-setup.ts Outdated
thymikee and others added 3 commits October 17, 2026 01:05
The global setup made every test related to every mutated source. Suites
that reach host-reading code through neither a daemon request nor the
CLI bind a host with bindTestCommandHost().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…kages

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… scenarios

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Pushed d522d257de: the global test setup is gone, so the related-test set no longer grows. Suites that need a host bind one with bindTestCommandHost() (test-kit); a new test pins host_environment_unscoped outside a host. The kernel-errors 3/5 mutation shard passes locally with 1371 related tests (main: 1369). Waiting on CI.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3763/

Built to branch gh-pages at 2026-10-17 05:33 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 97 files (changes from recent commits).

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/__tests__/cli-capture.ts
@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the PR. The host capture is a good direction, but one defect in the client needs a fix before merge. CI is green at d522d25 (23 checks, none failing), and there are no conflicts.

createAgentDeviceClient now accepts deps.host (or captures one at creation, line 89), and client.stateDir() resolves from host.env.AGENT_DEVICE_STATE_DIR and host.cwd (src/agent-device-client.ts#L215). Every command the same client sends goes through sendToDaemon, which still resolves the state dir from live process.env and process.cwd() (daemon-client-lifecycle.ts#L97, L107) and launches the daemon with process.env (line 818). Two inputs show the gap: createAgentDeviceClient({}, { host: { env: { AGENT_DEVICE_STATE_DIR: '/x' }, cwd } }), and setting process.env.AGENT_DEVICE_STATE_DIR (or calling chdir) after the client exists. Before this PR both reads were live and agreed. Now a public Node API client can report one state dir while it starts and talks to a daemon in another, so tooling that cleans or inspects client.stateDir() acts on the wrong daemon. The rule: everything one client reads must resolve from the same host. Either thread host through the transport factory into resolveClientSettings and startDaemon (explicit state dir, cwd, launch env), or drop deps.host and keep stateDir() on live process reads until #3751 moves the daemon client.

Not blocking, and you can take or leave these: RequestExecutionScope.host is set but nothing in production reads it, because daemon handlers call the ambient commandHostEnvironment() and the test only checks the field is the object passed in (request-execution-scope.ts:275); the MCP route runs inside the bin host scope but createClient calls createAgentDeviceClient(config) without deps.host, so it captures process again (src/mcp/command-tools.ts:251); ensureHarmonyToolchainPathConfigured writes env.PATH on the shared captured host, and captureHostEnvironment does not freeze its copy, so the Readonly type does not stop the write (packages/platform-harmonyos/src/hdc.ts:72); a relative AGENT_DEVICE_CLAIMS_DIR used to resolve against cwd and now throws relative_claims_dir, which the PR body and CHANGELOG do not mention, and the body also names a setup file that does not exist and says 178 files where the diff has 262 (device-claim-paths.ts:35); createTestHostEnvironment repeats captureHostEnvironment by hand (packages/test-kit/src/host-environment.ts:13); and the "request meta cwd or daemon cwd" fallback now reads inline process.cwd() in some daemon sites (screenshot-runtime.ts:326, record-runtime.ts:230/239, the session-script files, session-replay-transaction.ts:65) but commandHostEnvironment().cwd in others (session-perf-runtime.ts:149, session-open-execution.ts:432), so one source or a tracking line in #3751/#3752 would help.

Could the AsyncLocalStorage scope keep only what it already owns, which is spawn env (resolveSpawnEnv, commandDeveloperDir, exec trace), while handlers get the host explicitly? Daemon handlers would read scope.host or a host on their params, CLI ClientCommandHandlers would get ctx.host in the context they already receive, and platform modules would take env from their existing host ports, as Android does with AndroidAdbHost.environment. That would remove the throwing ambient getter and the 89 bindTestCommandHost suites, and make the dead scope.host field the real channel. Before #3750 and #3751 build on this, please decide in #3747 whether "bound HostEnvironment" means an ambient locator or explicit context fields.

I did not trace every public SDK export (artifacts, android-adb, limrun, ai-sdk) to prove none reaches commandHostEnvironment() outside a scope, where it would throw host_environment_unscoped. Unit tests cannot catch that, because 89 suites bind a live host. I also did not run code to check that the scope reaches HTTP connection callbacks and every daemon timer, though the request router rebinds per request. I did not run the tests, so the claim that cli-client-commands.test.ts fails without the harness binding is unverified. No live device run was asked for or checked.

This is reviewed at d522d25. Before merge, the client needs to resolve every state dir read from one host, and the scope question for #3750 and #3751 needs an answer.

createAgentDeviceClient's default transport, the daemon launch and the
CLI run's transport read the host the client was given, not the process.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files (changes from recent commits).

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/daemon-client/daemon-client-lifecycle.ts
Comment thread src/__tests__/daemon-registration-owner.test.ts
Comment thread src/cli.ts
Comment thread src/daemon-client/daemon-client.ts
@thymikee

Copy link
Copy Markdown
Member Author

Pushed 756cf4e012 for the client finding: createAgentDeviceClient's default transport is now createDaemonTransport(host), so state-dir resolution, remote and transport settings, the policy check, the daemon launch environment, the provider credential fingerprint and the developer dir all read the client's host. sendToDaemon keeps the released process-level default through a boundary wrapper; the CLI run builds its transport from the run's host. A new test (daemon-client-host.test.ts) gives the client a host that disagrees with process.env and checks that stateDir() and the settings the transport resolves agree.

Also taken from the non-blocking list: createTestHostEnvironment goes through captureHostEnvironment, and the PR body now names the relative_claims_dir refusal and no longer mentions the deleted setup file or the old file count. Not changed: the MCP route's createClient (its host is the same capture as the bin host), the Harmony PATH write (it deliberately changes the environment the daemon spawns with), and the inline process.cwd() fallbacks, which stay in the modules where an import would grow an eager closure; #3751/#3752 own migrating them.

On the ambient versus explicit question: the ambient commandHostEnvironment() exists because platform modules had no host to read and R13 forbids process there. I would like it to stay narrow, covering spawn env and the exec diagnostics, and have #3750 give platform modules their host through the ports they already own (as AndroidAdbHost.environment does), with handlers reading scope.host. That would let #3750/#3751 delete the accessor and the bindTestCommandHost suites as they go. It is your call, and I have asked in #3747 for the decision before they build on this.

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.

refactor(host-kit): capture HostEnvironment once at entry points

1 participant