Skip to content

skill required-secrets env injection silently no-ops through CompositeExecutor #3869

@bug-ops

Description

@bug-ops

Description

A skill that declares x-requires-secrets: <name> in SKILL.md is supposed to have the corresponding ZEPH_SECRET_<NAME> vault value injected into the shell subprocess as env var <NAME> (uppercase) for the duration of its activation (see crates/zeph-core/src/agent/tool_execution/mod.rs:542-582). In practice, the env var never reaches the subprocess in the production executor layout.

Verified live in TUI (zeph --tui --config ~/.config/zeph/gonkagate.toml, commit b668009):

skill=github confidence=0.98          # github skill matched and activated
bash $ env | grep -E '^GITHUB|^GH_' || echo NO_TOKEN
→ NO_TOKEN                            # GITHUB_TOKEN absent in subprocess env

Vault contains ZEPH_SECRET_GITHUB_TOKEN. zeph skill list confirms github — ... [trusted] (requires: github_token) — parsing and trust are fine. The "skill deactivated: missing required secrets" filter (crates/zeph-core/src/agent/context/assembly.rs:690-696) does NOT fire, so available_custom_secrets["github_token"] IS populated. Yet the env never lands in bash.

Root cause

CompositeExecutor in crates/zeph-tools/src/composite.rs implements ToolExecutor but does not override set_skill_env or set_effective_trust. It therefore inherits the default ToolExecutor impls (crates/zeph-tools/src/executor.rs:651 and :656), which are silent no-ops.

Production wiring (src/agent_setup.rs:534-541):

let base_executor = CompositeExecutor::new(
    file_executor,
    CompositeExecutor::new(
        shell_executor,
        CompositeExecutor::new(scrape_executor, cwd_executor),
    ),
);
let composite = CompositeExecutor::new(base_executor, mcp_executor);

When inject_active_skill_env calls self.tool_executor.set_skill_env(Some(env)), the call enters the outermost CompositeExecutor and is dropped on the floor — it never reaches ShellExecutor::set_skill_env (crates/zeph-tools/src/shell/mod.rs:433), so skill_env: RwLock<Option<HashMap>> stays None and the spawn-time resolve_context (shell/mod.rs:1097) finds nothing to insert.

The same defect breaks set_effective_trust: trust-gating for active skills is bypassed in the production executor layout. Any reasoning that relies on effective_trust reflecting the active skill's trust level (e.g., crates/zeph-tools/src/trust_gate.rs:107) is unreachable when CompositeExecutor is the outermost layer — which is always, in the current bootstrap.

Reproduction

  1. cargo run --features full -- vault set ZEPH_SECRET_GITHUB_TOKEN <token>.
  2. Add x-requires-secrets: github_token to ~/.config/zeph/skills/github/SKILL.md frontmatter.
  3. zeph skill trust github trusted.
  4. zeph --tui --config ~/.config/zeph/gonkagate.toml.
  5. Prompt the agent to run any bash command that introspects env, e.g. env | grep -E '^GITHUB|^GH_'.
  6. Observe: tool output shows no GITHUB_TOKEN. Logs show skill=github confidence=... (skill is active). No skill deactivated: missing required secrets line.

Expected Behaviour

Subprocess env contains GITHUB_TOKEN=<value> while github is among active_skill_names. Removing the skill from active set (or removing the secret from vault) reverts to no env injection.

Logs / Evidence

Saved artifacts:

  • .local/testing/debug/gh-tui-final.txt — TUI pane capture showing NO_TOKEN
  • .local/testing/debug/gh-skill-env-log-delta.log — log slice over the test turns

Fix Direction

CompositeExecutor must forward set_skill_env, set_effective_trust, and any other state-mutating non-default methods to both first and second (or to both with deduplication). A blanket-forwarding approach is safer than per-method overrides:

fn set_skill_env(&self, env: Option<HashMap<String, String>>) {
    self.first.set_skill_env(env.clone());
    self.second.set_skill_env(env);
}

fn set_effective_trust(&self, level: SkillTrustLevel) {
    self.first.set_effective_trust(level);
    self.second.set_effective_trust(level);
}

Test gap: there is no integration test asserting that set_skill_env propagates through the production layered executor. Adding one (e.g., spy at ShellExecutor level via the existing test infrastructure in crates/zeph-core/src/agent/tool_execution/tests/parallel_and_handle_tests.rs:549-642) would have caught this.

Environment

  • Version: commit b668009 on main
  • Config: ~/.config/zeph/gonkagate.toml (full feature set, sandbox network-allow-all, age vault)
  • Platform: macOS 25.4.0

Related security implication

Quarantined skills currently appear to permit bash (and other QUARANTINE_DENIED tools) because set_effective_trust is silently dropped at the CompositeExecutor boundary — TrustGateExecutor::effective_trust stays at its initial Trusted value regardless of the active skill's trust level. Worth verifying in a separate test before deciding whether to file a security advisory.

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