Merge pull request #49207 from kshitijk4poor/fix/cron-script-env-sanitize

fix(cron): sanitize env for job script subprocesses
This commit is contained in:
kshitij 2026-06-20 00:36:26 +05:30 committed by GitHub
commit 4c5217b717
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 37 additions and 4 deletions

View file

@ -121,10 +121,11 @@ outside the supported security posture.
### 2.3 Credential Scoping
Hermes Agent filters the environment it passes to its lower-trust
in-process components: shell subprocesses, MCP subprocesses, and
the code-execution child. Credentials like provider API keys and
gateway tokens are stripped by default; variables explicitly
declared by the operator or by a loaded skill are passed through.
in-process components: shell subprocesses, MCP subprocesses,
cron job scripts, and the code-execution child. Credentials like
provider API keys and gateway tokens are stripped by default;
variables explicitly declared by the operator or by a loaded
skill are passed through.
This reduces casual exfiltration. It is not containment. Any
component running inside the agent process (skills, plugins, hook

View file

@ -961,6 +961,10 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
Shell support lets ``no_agent=True`` jobs ship classic bash watchdogs
(the `memory-watchdog.sh` pattern) without wrapping them in Python.
Subprocess environment is passed through ``_sanitize_subprocess_env`` so
provider credentials and other Hermes-managed secrets are not inherited
(SECURITY.md §2.3), matching terminal and MCP child processes.
Args:
script_path: Path to the script. Relative paths are resolved
against HERMES_HOME/scripts/. Absolute and ~-prefixed paths
@ -1022,6 +1026,8 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
argv = [sys.executable, str(path)]
try:
from tools.environments.local import _sanitize_subprocess_env
popen_kwargs = {"creationflags": windows_hide_flags()} if sys.platform == "win32" else {}
result = subprocess.run(
argv,
@ -1029,6 +1035,7 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
text=True,
timeout=script_timeout,
cwd=str(path.parent),
env=_sanitize_subprocess_env(os.environ.copy()),
**popen_kwargs,
)
stdout = (result.stdout or "").strip()

View file

@ -132,6 +132,31 @@ class TestRunJobScript:
assert "exited with code 1" in output
assert "error info" in output
def test_script_subprocess_env_sanitized(self, cron_env, monkeypatch):
"""Cron scripts must not inherit Hermes provider env (SECURITY.md §2.3)."""
from tools.environments.local import _HERMES_PROVIDER_ENV_BLOCKLIST
from cron.scheduler import _run_job_script
# sorted() so the probed var is deterministic across runs
# (frozenset iteration order varies with PYTHONHASHSEED).
blocked_var = sorted(_HERMES_PROVIDER_ENV_BLOCKLIST)[0]
monkeypatch.setenv(blocked_var, "must_not_leak")
script = cron_env / "scripts" / "env_probe.py"
script.write_text(
textwrap.dedent(
f"""\
import os
key = {blocked_var!r}
print("PRESENT" if os.environ.get(key) else "ABSENT")
"""
)
)
success, output = _run_job_script("env_probe.py")
assert success is True
assert output == "ABSENT"
def test_script_empty_output(self, cron_env):
from cron.scheduler import _run_job_script