Skip to content

macOS sandbox rewrite drops Command env overrides (skill secret env lost)  #3871

@bug-ops

Description

@bug-ops

Description

After #3870 the set_skill_env call propagates through CompositeExecutor and lands on ShellExecutor, which in build_bash_command (crates/zeph-tools/src/shell/mod.rs:2515) sets the extra env via cmd.envs(extra_env). However, when [tools.sandbox] enabled = true on macOS, the env overrides are dropped by the sandbox rewrite.

In crates/zeph-tools/src/sandbox/macos.rs:341-376, rewrite_command_with_sandbox_exec replaces the entire std::process::Command with a fresh one to prepend sandbox-exec:

*std_cmd = std::process::Command::new(sandbox_exec);   // <-- env overrides lost here
std_cmd.arg("-f");
std_cmd.arg(profile_path);
std_cmd.arg("--");
std_cmd.arg(original_program);
for arg in original_args { std_cmd.arg(arg); }

Only program and args are carried over. The envs map, env_remove entries, and current_dir are not. Any per-call env override (e.g. GITHUB_TOKEN from skill x-requires-secrets) is silently lost; the spawned sandbox-exec then inherits only the parent agent's env, where the secret is intentionally absent.

End-to-end symptom: with github skill active, x-requires-secrets: github_token declared, ZEPH_SECRET_GITHUB_TOKEN in age vault, and #3870 applied, gh auth status inside the agent reports:

github.com
  X Failed to log in to github.com account bug-ops (default)
  - Active account: true
  - The token in default is invalid.

(default) is gh's marker for "no token from env, fell back to hosts.yml" — confirming GH_TOKEN/GITHUB_TOKEN never reached the subprocess. Direct invocation GH_TOKEN=<vault_value> gh auth status succeeds with (GH_TOKEN) as source, proving the token itself is valid and the loss is purely in the sandbox rewrite path.

Same defect affects:

  • current_dircmd.current_dir(p) set by callers before apply_sandbox is wiped.
  • Any future per-call env config (e.g. injected RUST_LOG, proxy vars).
  • Linux Landlock path (linux.rs) likely fine because it does not rebuild the Command — needs verification.

Reproduction (macOS)

  1. Apply fix(tools): forward set_skill_env and set_effective_trust through CompositeExecutor #3870, set [tools.sandbox] enabled = true, profile = "network-allow-all" and default_level = "trusted" for [skills.trust].
  2. cargo run --features full -- vault set ZEPH_SECRET_GITHUB_TOKEN <gh_token>.
  3. Add x-requires-secrets: github_token to ~/.config/zeph/skills/github/SKILL.md frontmatter; ensure github skill is trusted.
  4. Launch TUI, ask agent to run gh auth status (skill matcher should activate github at high confidence).
  5. Output shows token source (default) and "The token in default is invalid", confirming GITHUB_TOKEN not in subprocess env.

Counter-test (proves token is valid and sandbox is the problem):

  • Disable sandbox: [tools.sandbox] enabled = false.
  • Same prompt → gh auth status source is (GH_TOKEN) and gh works.

Expected Behaviour

sandbox-exec is spawned with the same env overrides, env_remove entries, and current_dir that were set on the original Command. The sandbox wrap should be purely a program/args prepend; it MUST NOT discard caller-configured execution context.

Fix Direction

Capture and replay env state across the Command replacement. std::process::Command exposes:

  • get_envs() -> CommandEnvs<'_> — yields (&OsStr, Option<&OsStr>); Some(v) means override-to-v, None means env_remove.
  • get_current_dir() -> Option<&Path>.
  • get_env_clear() -> bool (nightly only — workaround: re-apply env_clear if originally requested via a separate flag).

Patch in rewrite_command_with_sandbox_exec:

let original_program = std_cmd.get_program().to_os_string();
let original_args: Vec<OsString> = std_cmd.get_args().map(OsStr::to_os_string).collect();
let original_envs: Vec<(OsString, Option<OsString>)> = std_cmd
    .get_envs()
    .map(|(k, v)| (k.to_os_string(), v.map(OsStr::to_os_string)))
    .collect();
let original_cwd = std_cmd.get_current_dir().map(Path::to_path_buf);

*std_cmd = std::process::Command::new(sandbox_exec);
std_cmd.arg("-f");
std_cmd.arg(profile_path);
std_cmd.arg("--");
std_cmd.arg(original_program);
for arg in original_args { std_cmd.arg(arg); }
for (k, v) in original_envs {
    match v {
        Some(val) => { std_cmd.env(k, val); }
        None      => { std_cmd.env_remove(k); }
    }
}
if let Some(cwd) = original_cwd { std_cmd.current_dir(cwd); }

Test gap: add a unit test that constructs a Command::new("bash").arg("-c").arg("…").envs([("FOO", "bar")]).current_dir("/tmp"), runs it through rewrite_command_with_sandbox_exec, then asserts via get_envs / get_current_dir that both survived the rewrite.

Environment

Logs / Evidence

Tool result captured from live TUI (CI-784 session):

$ gh auth status
[stderr] github.com
[stderr]   X Failed to log in to github.com account bug-ops (default)
[stderr]   - Active account: true
[stderr]   - The token in default is invalid.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High ROI, low complexity — do next sprintbugSomething isn't workingskillszeph-skills crate

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions