From e4b9532c1827e3c51ca03e6e35512d2cade4d905 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 29 May 2026 04:10:05 -0700 Subject: [PATCH] feat: embedder environment-hint hook for the system prompt (#34574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(security): block AWS SDK creds from subprocess env * fix(security): narrow Bedrock subprocess strip to inference bearer token only Scopes the AWS_SDK subprocess strip down from the full AWS credential chain to just AWS_BEARER_TOKEN_BEDROCK — the only Hermes-managed *inference* secret (analogous to OPENAI_API_KEY). The general AWS credential chain (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN / AWS_PROFILE / config + role pointers) is intentionally left inheritable. Why: per SECURITY.md §3.2 the local terminal is the user's trusted operator shell. Hard-blocklisting the general chain would (a) regress *every* user who runs aws/terraform/cdk/boto3 in the agent terminal — not just Bedrock users, since PROVIDER_REGISTRY is iterated unconditionally at import — and (b) be unrecoverable, because env_passthrough.py refuses to re-allow anything in _HERMES_PROVIDER_ENV_BLOCKLIST (GHSA-rhgp-j443-p4rf). The narrow strip closes the reported leak (opencode enumerating the Bedrock catalog off the leaked bearer token) with no capability loss. Keeps zapabob's self-healing auth_type=="aws_sdk" mechanism so any future SDK-cred provider is covered automatically. Tests: bearer token stripped + general chain preserved (no-regression guard), on both the runtime strip path and the blocklist-membership path. Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp> * feat: embedder environment-hint hook for the system prompt Adds HERMES_ENVIRONMENT_HINT env var (and config.yaml agent.environment_hint) so a host wrapping Hermes (sandbox runner, managed platform) can describe the runtime environment — proxy, credential handling, mount layout — in the system prompt's environment-hints block, without editing the identity slot (SOUL.md). Read once at prompt-build time, so it lands in the stable, cache-safe portion of the system prompt. Env var overrides the config key (build-time/container mechanism). Empty by default — no behavior change for existing installs. --------- Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp> --- agent/prompt_builder.py | 21 ++++++++++++ hermes_cli/config.py | 7 ++++ tests/agent/test_prompt_builder.py | 52 ++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 0f9822804..7ba2edfa1 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -848,6 +848,27 @@ def build_environment_hints() -> str: if is_wsl(): hints.append(WSL_ENVIRONMENT_HINT) + + # Embedder-supplied environment description. Lets a host that wraps Hermes + # (e.g. a sandbox runner / managed platform) explain the environment the + # agent is running in — proxy, credential handling, mount layout — without + # forking the identity slot (SOUL.md). Read once at prompt-build time, so + # it's part of the stable, cache-safe system prompt. The env var is the + # build-time/embedder mechanism (set in a container ENV); config.yaml + # ``agent.environment_hint`` is the user-facing surface. Env var wins. + extra = (os.getenv("HERMES_ENVIRONMENT_HINT") or "").strip() + if not extra: + try: + from hermes_cli.config import load_config + + extra = str( + (load_config().get("agent", {}) or {}).get("environment_hint", "") + ).strip() + except Exception as e: + logger.debug("Could not read agent.environment_hint from config: %s", e) + if extra: + hints.append(extra) + return "\n\n".join(hints) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 690e00d9f..e2c59a694 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -683,6 +683,13 @@ DEFAULT_CONFIG = { # (docker/modal/ssh — they have their own probe). Set False to # disable entirely. "environment_probe": True, + # Embedder-supplied environment description appended to the system + # prompt's environment-hints block. Lets a host that wraps Hermes + # (sandbox runner, managed platform) explain the runtime environment + # — proxy, credential handling, mount layout — without editing the + # identity slot (SOUL.md). Empty by default. The HERMES_ENVIRONMENT_HINT + # env var overrides this (build-time/container mechanism). + "environment_hint": "", # Staged inactivity warning: send a warning to the user at this # threshold before escalating to a full timeout. The warning fires # once per run and does not interrupt the agent. 0 = disable warning. diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index e0370c309..f309c84e2 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -947,6 +947,58 @@ class TestEnvironmentHints: f"info is suppressed in the system prompt" ) + def test_environment_hint_from_env_var_is_appended(self, monkeypatch): + """HERMES_ENVIRONMENT_HINT lets an embedder describe the runtime env.""" + import agent.prompt_builder as _pb + monkeypatch.setattr(_pb, "is_wsl", lambda: False) + monkeypatch.delenv("TERMINAL_ENV", raising=False) + monkeypatch.setenv("HERMES_ENVIRONMENT_HINT", "Running inside an OpenShell sandbox.") + _pb._clear_backend_probe_cache() + result = _pb.build_environment_hints() + assert "Running inside an OpenShell sandbox." in result + # The factual host block must still come first. + assert result.index("Host:") < result.index("OpenShell") + + def test_environment_hint_env_var_overrides_config(self, monkeypatch): + """Env var wins over config.yaml agent.environment_hint.""" + import agent.prompt_builder as _pb + monkeypatch.setattr(_pb, "is_wsl", lambda: False) + monkeypatch.delenv("TERMINAL_ENV", raising=False) + monkeypatch.setenv("HERMES_ENVIRONMENT_HINT", "ENV-WINS") + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"agent": {"environment_hint": "CONFIG-VALUE"}}, + ) + _pb._clear_backend_probe_cache() + result = _pb.build_environment_hints() + assert "ENV-WINS" in result + assert "CONFIG-VALUE" not in result + + def test_environment_hint_falls_back_to_config(self, monkeypatch): + """With no env var, the config.yaml value is used.""" + import agent.prompt_builder as _pb + monkeypatch.setattr(_pb, "is_wsl", lambda: False) + monkeypatch.delenv("TERMINAL_ENV", raising=False) + monkeypatch.delenv("HERMES_ENVIRONMENT_HINT", raising=False) + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"agent": {"environment_hint": "CONFIG-VALUE"}}, + ) + _pb._clear_backend_probe_cache() + result = _pb.build_environment_hints() + assert "CONFIG-VALUE" in result + + def test_environment_hint_empty_by_default(self, monkeypatch): + """No hint configured anywhere → no embedder text, host block intact.""" + import agent.prompt_builder as _pb + monkeypatch.setattr(_pb, "is_wsl", lambda: False) + monkeypatch.delenv("TERMINAL_ENV", raising=False) + monkeypatch.delenv("HERMES_ENVIRONMENT_HINT", raising=False) + monkeypatch.setattr("hermes_cli.config.load_config", lambda: {"agent": {}}) + _pb._clear_backend_probe_cache() + result = _pb.build_environment_hints() + assert "Host:" in result + # ========================================================================= # Conditional skill activation