fix(host): forward SSH_AUTH_SOCK to runners and harness CLIs - #4377
Conversation
Every runner-spawned context lost the ssh-agent socket, so any agent doing git-over-SSH or SSH-cert-authenticated tooling failed with "dial unix: missing address" (often surfacing as a confusing 401 from the endpoint, since such tools have no cached-token fallback). Two independent gates dropped it: - `_build_runner_env` filters the host env through `_RUNNER_ENV_ALLOWLIST`, which omitted SSH_AUTH_SOCK. This is also the list both host-daemon modes consult, so the one entry fixes the daemon hop too, including remote mode. - `clean_agent_env` is the shared deny-by-default filter for every vendor CLI, and its safe base omitted it. Fixing the shared base covers all seven harnesses rather than only the one whose report surfaced this. Classified as a path, not a bearer secret: it names a unix socket, and reaching the agent behind it still requires the user's own ssh-agent to be running and holding the key. Same footing as KUBECONFIG, already allowlisted. An ACTIVE OS sandbox deliberately keeps excluding it: that boundary exists to confine the agent, and signing with the user's keys is what it confines. `os_env.py` previously justified its exclusion by calling the variable "a credential surface masquerading as a path", which contradicts the classification above; that rationale is rewritten to rest on the sandbox boundary instead, so the codebase states one position. Downstream paths needed no change: `sys_os_shell` (sandbox inactive) and `sys_terminal_launch` both mirror the parent env, so they inherit the fix. Codex's `shell_environment_policy.inherit` was reported as a third gate requiring omnigent to force `inherit="all"`. It does not reproduce: on codex-cli 0.144.3 the default already passes SSH_AUTH_SOCK through (identical 72-var env), and only an explicit `inherit="core"` drops it. Forcing `all` would override that deliberate user choice, so no override is added. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
781db8b to
bc3c5f6
Compare
|
🏷️ Doc impact: Adds SSH_AUTH_SOCK to internal env-propagation allowlists so ssh-agent auth survives runner/harness/sandbox boundaries — an internal fix that doesn't change any documented user-facing surface or default. Auto-classified on merge. Set the label manually before merging to override. · run |
Forward-port PR #82 onto the upstream-0.9 reconcile base (64216aa) preserving all newer upstream changes including: - SSH_AUTH_SOCK in _RUNNER_ENV_ALLOWLIST (omnigent-ai#4377) - workspace cwd in _spawn_runner_proc (#3419de8d) - codex spec= parameter in resolve_native_codex_launch (omnigent-ai#4208) - runner event rejection as failed/ERROR (omnigent-ai#4354) - pi-native inline family from selected model's family (omnigent-ai#4348) The picker semantics from PR #82 are preserved: - pi-native supports host.model_options (prelaunch model picker) - pi_native_provider_launch gets an explicit selection arg so the rendered provider matches the user's pick and a bad selection cannot silently fall back to an unrelated Pi login - inline picker renders in the new-session composer before opening the gear modal; both share the same model state - harness changes (Pi/Codex/Claude) change the catalog - Default omits model_override; explicit pick persists it - provider/catalog failures surface status='failed' rather than fabricating a default Tests: - 76 pi-native credentials tests pass - 40 pi-native extension tests pass - 58 native-terminal autocreate tests pass - 57 web flow tests pass (NewChatDialog) - All 3 pre-existing host connect environment failures unchanged (Path.home() tilde expansion issues) - not regressions
Forward-port PR #82 onto the upstream-0.9 reconcile base (64216aa) preserving all newer upstream changes including: - SSH_AUTH_SOCK in _RUNNER_ENV_ALLOWLIST (omnigent-ai#4377) - workspace cwd in _spawn_runner_proc (#3419de8d) - codex spec= parameter in resolve_native_codex_launch (omnigent-ai#4208) - runner event rejection as failed/ERROR (omnigent-ai#4354) - pi-native inline family from selected model's family (omnigent-ai#4348) The picker semantics from PR #82 are preserved: - pi-native supports host.model_options (prelaunch model picker) - pi_native_provider_launch gets an explicit selection arg so the rendered provider matches the user's pick and a bad selection cannot silently fall back to an unrelated Pi login - inline picker renders in the new-session composer before opening the gear modal; both share the same model state - harness changes (Pi/Codex/Claude) change the catalog - Default omits model_override; explicit pick persists it - provider/catalog failures surface status='failed' rather than fabricating a default Tests: - 76 pi-native credentials tests pass - 40 pi-native extension tests pass - 58 native-terminal autocreate tests pass - 57 web flow tests pass (NewChatDialog) - All 3 pre-existing host connect environment failures unchanged (Path.home() tilde expansion issues) - not regressions
Related issue
Closes #3531
Summary
Every runner-spawned context lost the ssh-agent socket, so any agent doing git-over-SSH or SSH-cert-authenticated tooling failed with
dial unix: missing address. In certificate-based auth flows this often surfaces as a confusing401 Unauthorizedfrom the endpoint rather than an SSH error, because such tools hard-fail without the socket and have no cached-token fallback.ELI5: your ssh-agent is a doorman holding your keys, and
SSH_AUTH_SOCKis the intercom number for reaching them. Omnigent was handing agents a blank intercom number, so they'd knock on the door and get turned away, without any hint that the number was the problem.Two independent gates dropped it:
_build_runner_envfilters the host env through_RUNNER_ENV_ALLOWLIST, which omittedSSH_AUTH_SOCK. This is also the list both host-daemon modes consult, so this one entry fixes the remote-daemon hop too (issue caveat Sync 🌊: upstream catch-up #3).clean_agent_envis the shared deny-by-default filter, and its safe base omitted it.Classified as a path, not a bearer secret: it names a unix socket, and reaching the agent behind it still requires the user's own ssh-agent to be running and holding the key. Same footing as
KUBECONFIG, already allowlisted on exactly that reasoning.An active OS sandbox deliberately keeps excluding it: that boundary exists to confine the agent, and signing with the user's keys is what it confines.
os_env.pypreviously justified its exclusion by calling the variable "a credential surface masquerading as a path", contradicting the classification above; that rationale is rewritten to rest on the sandbox boundary, so the codebase states one position instead of both.Two deliberate divergences from the scope proposed in this comment
Gate 2 is fixed in the shared base, not in codex. The proposal was to patch codex's
_clean_codex_env. That function is a thin wrapper overclean_agent_env, which is the spawn-env filter for all seven harnesses. Patching only codex would have left the other six broken while looking fixed, so the fix lands one level down. Smaller diff, and it's the actual root cause.Gate 3 (
shell_environment_policy.inherit="all") is intentionally NOT added, because its premise does not reproduce. On codex-cli 0.144.3 the default already passesSSH_AUTH_SOCKthrough; only an explicitinherit="core"drops it. Measured with a cleanCODEX_HOMEto rule out local config:inheritSSH_AUTH_SOCKpresentallcorenoneSince default and
allare identical, forcingallwould buy nothing for the common case, and for the one user who did setinherit = "core"it would silently override a deliberate narrowing of their own env. Omnigent sets noshell_environment_policyanywhere and copies the user'sconfig.toml, so that setting stays the user's call. If someone reproduces a drop on a codex version where the default differs, that's worth a follow-up issue rather than a pre-emptive override here.Test Plan
pytest tests/test_agent_spawn_env_canary.py \ tests/host/test_connect.py::test_build_runner_env_allowlists_host_env_and_strips_secretstest_real_builders_pass_ssh_auth_sockdrives all seven harnesses' real spawn-env builders, so a future harness that forgets to filter correctly fails here. Sits beside the existingNODE_EXTRA_CA_CERTScanary, which guards the same "must survive filtering" property.test_agent_spawn_env_canary.py(38),test_connect.py,test_os_env*.py, plus goose/kimi/hermes/acp/pi executor suites (354).DROPPEDbefore the change and the socket path after. Confirmedsys_os_shellwas innocent (passes through both before and after), matching the issue's analysis.sys_terminal_launchis a plainos.environ.copy()with no name filtering, so it inherits gate 1 rather than needing its own entry.Two pre-existing failures on this branch, both reproduced on stashed clean
mainand unrelated to this change:test_connect.py::test_run_host_process_announces_session_log_dir_on_startandtest_codex_hooks_generation.py::test_router_hook_survives_a_shadowing_workspace.Demo
N/A (no user-visible UI; the change is an env-propagation fix).
Type of change
Test coverage
Coverage notes
Automated coverage pins both gates: the per-harness canary covers gate 2 for all seven harnesses, and the allowlist test covers gate 1.
Manual verification was the codex
inheritmatrix above (realcodex sandboxinvocations against a cleanCODEX_HOME) plus the scratch before/after repro of the two gates. The codex matrix is deliberately not a test: it asserts third-party CLI default behaviour that we don't control and would turn into a false alarm on a vendor version bump.Not covered automatically: a real end-to-end
ssh -T git@github.comthrough a live ssh-agent inside a runner-spawned pane, which needs a real agent and network. Reviewers wanting that check can startomnigent hostwithout theOMNIGENT_RUNNER_ENV_PASSTHROUGH=SSH_AUTH_SOCKworkaround and runecho $SSH_AUTH_SOCK && ssh -T git@github.comin bothsys_os_shelland asys_terminal_launchpane.Changelog
Agents now inherit your ssh-agent, so git-over-SSH and SSH-cert-authenticated tooling work in agent shells and terminals