Repository navigation
fix(runstore): keep the host session out of a detached run's env (#1082) - #1088
Conversation
…hed runs (#1082) The MCP surface refused a write outside AGENTDESCENT_SURFACE_WRITE_DIRS (#1018), but `agentdescent apply` passed no check, so a host worker that could not write through the tool could still write through the CLI. Pass the same allowlist check at the CLI's write-advertising paths: `apply` (run and `--from`, dry run included) and `show --diff`'s `apply_plan`, which now carries the refusal instead of a path the worker cannot use. Unset means unchanged CLI behaviour; an empty value refuses every write. `runstore.launch` also copied all of `os.environ` into the detached run, so the worker session's `DSH_*`/`CLAUDE*`/`CODEX*`/`MCP_*` variables -- including its MCP server config -- reached the run. Drop those prefixes with `surface_common.scrub_host_env`; the API keys and PATH the run needs still pass through. Refs #1082: the per-Ticket budget-cap row is not in this change.
) Row 3 of #1082: the run `launch` detaches is our own process, so it inherits all of os.environ -- including the worker session's DSH_*/CLAUDE*/CODEX*/MCP_* variables and its MCP server config. Drop those prefixes with surface_common.scrub_host_env; the API keys and PATH the run needs still pass through. Row 1 (the CLI write allowlist) already has its own carrier in #1087, so this change does not touch it. Refs #1082: the per-Ticket budget-cap row is still open.
|
Verdict: CHANGES REQUESTED Tip SHA: Ran:
Suggested fix: derive the scrub from |
…d env (#1082) `scrub_host_env` carried its own four prefixes while the candidate worker (`agents.worker_env`) dropped eleven markers, so five of the eleven crossed the scrub: `OPENAI_AGENT_*`, `OPENCODE_CONFIG`, `OPENCODE_CONFIG_CONTENT`, `OPENCODE_CONFIG_DIR` and `XDG_CONFIG_HOME`. The two OpenCode config carriers are the ones measured to hand a nested process the parent's MCP servers, which is the recursion this guard exists for. The tuple now lives in `surface_common` (the leaf) and `agents` imports it, so there is one list. `launch` also scrubs the merged dict: `env=` is spread last and could put `MCP_*`/`DSH_*` back in after the scrub. The test walks `SESSION_MARKERS` instead of naming variables, so a marker added for a new host is covered the day it is added.
`scrub_host_env` drops `SESSION_MARKERS`, and that list contains `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `DSH_HOME` (the first by name, the other two behind the `CODEX_`/`DSH_` prefixes). They name a host's *configuration*, not its session: the detached run is this package, and `integrations._host_root` resolves a host's home from them, so a worker spec with `isolate: false` -- and every `agentdescent start` from a plain terminal -- was sent to `~/.claude` / `~/.codex`, which for a custom directory is the wrong, often unauthenticated, login (#1082 review, finding 1). `DETACHED_KEEP_VARS` holds the three names and `scrub_host_env` keeps them by exact name; the rest of `SESSION_MARKERS` still goes. The tuple lives in `surface_common` rather than being imported from `agents`, because this module stays a leaf: a test binds the set to `agents.WORKER_CONFIG_DIRS` instead. `is_session_marker` is unchanged, so an isolated worker still redirects all five directories into its own workspace and keeps none of the user's config. Refs #1082.
`integrations._host_root` reads four variables, not three: `DSH_HOME`, `CODEX_HOME`, `XDG_CONFIG_HOME` (for `_opencode_root`) and `CLAUDE_CONFIG_DIR`. `DETACHED_KEEP_VARS` held the other three, so an OpenCode host with a custom `XDG_CONFIG_HOME` still resolved `<home>/.config/opencode` after the scrub -- the same defect as the three already fixed (#1082 review, finding 1). The keep set now holds all four. Isolation is unchanged: `worker_env` filters by `is_session_marker`, not by the keep list, so an `isolate=True` worker is still redirected to `.agentdescent-worker/<leaf>` inside its workspace, and that row asserts where each value points; `isolate=False` keeps the host values, which is the contract the keep set exists for. The drift row reads the `_host_root` call sites out of `integrations`' own source and checks both directions against `agents.WORKER_CONFIG_DIRS`, so a fifth host root or a renamed one fails it. Refs #1082.
|
Record for the tip under review here: The last verdict on this PR names
Both findings are fixed at
|
|
Verifier report (advisory — not a counting PASS; Review tip: This is the tip after Birfy's CHANGES REQUESTED at Ran:
Not run: full Findings Blocking: none. Non-blocking
Risk / merge: T3 by path ( |
|
Verdict: PASS Tip SHA: Ran:
Not run: full This is the tip after CHANGES REQUESTED at
Minor, not blocking: the
|
Partial:
#1082— row 3. Row 1 (the CLI write allowlist) is#1087, merged asf5e93335; row 2 (per-Ticket budget caps) stays open on the issue.What changed
runstore.launchbuilt the detached run's environment from all ofos.environ, so the worker's own session leaked into the run: itsDSH_*markers, itsCLAUDE*/CODEX*host markers, and anyMCP_*server configuration that would hand the run a second set of tools. The run is our own process, so the candidate allowlist (runners._child_env) cannot be reused — it would drop the API keys andPATHthe run needs.agentdescent/surface_common.py: the package's one host-session denylistSESSION_MARKERS,is_session_marker, theDETACHED_KEEP_VARSkeep set, andscrub_host_env(environ=None).agentdescent/agents.py: imports the list fromsurface_commoninstead of carrying a second copy.agentdescent/runstore.py:launchbuilds the child env withscrub_host_env({**os.environ, ...}), soenv=cannot put the session back.tests/test_host_env_scrub.py(new): drop, keep, one-list, four-host-root, isolation and launch rows.docs/dsh-super-agent.md§5 andchangelog.d/1082-host-env-scrub.md.No model or network call.
cli.pyis untouched: theapply/show --diffwrite allowlist is#1087's row, and the budget caps are row 2.Review findings
integrations._host_rootreads four variables —DSH_HOME,CODEX_HOME,XDG_CONFIG_HOME(for_opencode_root) andCLAUDE_CONFIG_DIR— soDETACHED_KEEP_VARSkeeps all four by exact name. Everything else inSESSION_MARKERSstill goes,OPENCODE_CONFIG_DIRandOPENCODE_CONFIG_CONTENTincluded. The measurement is under "Verification at the tip": all four survive with their values, the four roots resolve against them, and the session markers are gone.SESSION_MARKERSlives insurface_commonandagentsimports it; a row asserts the two names are the same object, and the parametrised row walks the tuple, so a marker added for a new host is covered the day it is added.Red at base
c94e9d67Fresh tree at the base this PR targets, with only
tests/test_host_env_scrub.pycopied in:Red before the keep sets
At
c5f021e9, with no keep set:CLAUDE_CONFIG_DIR survives: False,CODEX_HOME survives: False,DSH_HOME survives: False,XDG_CONFIG_HOME survives: False, and_opencode_rootreturned<home>/.config/opencodewhileXDG_CONFIG_HOMEpointed at<home>/.config-work.At
50bde3c2, with the keep set holding three and the four-variable rows in the tree:pytest -q tests/test_host_env_scrub.py->4 failed, 18 passed, every failureKeyError: 'XDG_CONFIG_HOME'.Tip SHA: 85b2974
Commits:
b5b66bf8(the original scrub),9766bad1(forward-merge ontoc94e9d67),c5f021e9(one list, andlaunchre-scrubs the merged env),50bde3c2(keep the three config dirs),85b2974(keepXDG_CONFIG_HOME, isolation rows asserted by value). Fast-forward only: no force, no rebase, no merge-up.Verification at the tip
Ran:
CLAUDE_CONFIG_DIR,CODEX_HOME,DSH_HOME,XDG_CONFIG_HOMEsurvive with their values;CLAUDECODE,CODEX_SESSION_ID,DSH_SESSION_ID,MCP_SERVERS,OPENAI_AGENT_IDdroppedintegrations._host_rootre-resolved with the scrubbed env in place ->claude <home>/.claude-work,codex <home>/.codex-work,dsh <home>/.dsh-work,opencode <home>/.config-work/opencodepytest -q tests/test_host_env_scrub.py-> pass (22 passed)test_host_env_scrub,test_runstore_cli,test_runstore_tilde,test_cli_surface_p1,test_mcp_write_confinement) -> pass (102 passed)pytest -q tests/test_plugin_runner.py-> pass (15 passed)python -m tools.gen_api_docs --check-> pass (docs/api.md is up to date)python -m tools.changelog_fragments check-> pass (40 fragments OK)mkdocs build --strict-> exit 0 (13.47 s)pytest -qat this tip: the 492 files fromls tests/test_*.py | sort, split into 8 even chunks and run in parallel aspytest -q <chunk files>with anAGENTDESCENT_HOMEper chunk -> 7418 passed, 12 failed, 48 skipped, 10 errors. The red set is the environment set:test_era_srbench6,test_genesis_example2,test_sandbox_container2 failed + 10 errors (the docker image cannot be pulled on this host),test_wheel_smoke1, andtest_integrations::test_the_launcher_install_writes_ignores_a_shadowing_cwd1, which fails identically at basec94e9d67(a stale console script in this box's venv). None of them is inrunstore.py,surface_common.py,agents.pyortests/test_host_env_scrub.py.Not run:
test_era_srbench,test_genesis_example, wheel-smoke and read-only-$HOMElegs -> environmental on this hostSize: 6 files,
+397/-27. T3: over the 250-line cap, andrisk:T3from the controller matches.