Let config.toml carry the daemon's environment - #1169
Merged
Conversation
A key exported after the daemon started never reaches it: the daemon's environment is fixed at launch, and `construct daemon restart` re-execs the running image, carrying that same environment across. So setting DEEPSEEK_API_KEY made smith work in a fresh shell while the router's pickers stayed empty, with nothing to explain the gap — and no number of restarts fixed it. config.toml doesn't have that problem; it is re-read on every start. Add `[daemon.env]`, a table layered underneath the real environment: it fills gaps, never overrides, so a machine that exports its keys behaves exactly as before and an operator can still override a declared value for one run. The layer is consulted wherever the daemon resolves a credential — built-in route targets, profiles that name a variable or fall back to a provider's defaults, /configure, and doctor (which loads the same config with no daemon running, so it keeps reporting what the daemon would see) — and is applied as the base environment of every process the daemon spawns, so a declared credential reaches a harness exactly as an exported one would. That floor goes on at spawn rather than in the session's env map: that map is persisted as start params, and baking credentials into per-session state would both leak them to disk and freeze them against a rotation in config. Path overrides and the daemon's own CONSTRUCT_* knobs stay environment-only — they are read while locating the config file, before there is a table.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A credential exported after the daemon started never reaches it. The daemon's environment is fixed at launch, and
construct daemon restartre-exec()s the running image (crates/daemon/src/lib.rs), carrying the same environment across — so no number of restarts helps.The failure is silent and inverted from the user's mental model. Setting
DEEPSEEK_API_KEYmakes smith work in a fresh shell immediately, while the route pickers stay empty, with nothing on screen to explain why. Found while debugging exactly that: the key was in~/.zshrc, every hand-run tool could see it, and the one long-lived process that needed it could not.The change
config.tomldoesn't have this problem — it's re-read on every start. This adds[daemon.env]:layered underneath the real environment. It fills gaps and never overrides, so a machine that exports its keys behaves exactly as before, and an operator can still override a declared value for a single run.
The layer is consulted wherever the daemon resolves a credential — built-in route targets (spec 0179), profiles that name an
api_key_envor fall back to a provider's defaults,/configure, anddoctor(which loads the same config with no daemon running, so it keeps reporting what the daemon would see, per spec 0168) — and is applied as the base environment of every process the daemon spawns, so a declared credential reaches a harness exactly as an exported one would.Two deliberate boundaries:
CONSTRUCT_*knobs stay environment-only — they're read while locating the config file, before there is a table to consult. The config comments say so explicitly.Spec:
specs/0180-daemon-environment-from-config.md.Verification
cargo test --workspaceis green (0 failures). Nine new tests cover the precedence rules, blank-value handling, the built-in target appearing from config, a namedapi_key_envresolving through the layer, and/configurereporting a config-declared key as present.Also checked live against an isolated daemon whose real environment has no
DEEPSEEK_API_KEY, with the key declared only inconfig.toml:Negative control — same daemon, declaration removed — reports
DEEPSEEK MISSINGand[--] DEEPSEEK_API_KEY not set.No recording: the change has no distinct visual surface (the picker entry it produces already looks the way it looks today).
Relevant binary
Daemon-only change →
construct(.claude/worktrees/daemon-env-from-config/target/debug/construct).