fix(attribution): correct AI_AGENT id to registry value and carry harness markers into all terminal backends

The Hugging Face agent-harness registry matches standard-var values
EXACTLY against the harness id. Our registry id is 'hermes-agent'
(huggingface.js agent-harnesses.ts), so AI_AGENT=hermes was counted as
'unknown' — fixed at both entry points.

Remote terminal backends (Docker/SSH/Modal/Daytona/Singularity/Vercel)
never inherit the Hermes process env, and the cross-session leak guard
deliberately strips HERMES_SESSION_* from subprocess envs in engaged
multi-session hosts — so hf/huggingface_hub traffic from those shells was
unattributable. _wrap_command now exports AI_AGENT/HERMES_AGENT inside
every wrapped command with ${VAR:-default} semantics (outer harness is
never clobbered), and the snapshot dump excludes both names so a baked
value can never shadow a later outer harness.

E2E: verified against real huggingface_hub 1.27.0 detect_agent() with a
cached registry — 'hermes-agent' detected via AI_AGENT and via
HERMES_SESSION_ID; old 'hermes' value reproduced the 'unknown' bug.
This commit is contained in:
Teknium
2026-08-10 10:55:43 -07:00
parent e47a931d33
commit e5bc6b2186
5 changed files with 96 additions and 9 deletions
+4 -3
View File
@@ -27810,9 +27810,10 @@ def main():
# Advertise the agent harness to child processes (AI_AGENT is the
# cross-agent standard; HERMES_AGENT the Hermes-specific marker — see
# _advertise_agent_env in hermes_cli/main.py, kept inline here to avoid
# importing that module's startup side effects). setdefault so an outer
# harness is never clobbered.
os.environ.setdefault("AI_AGENT", "hermes")
# importing that module's startup side effects). The value must equal our
# public agent-harness registry id (``hermes-agent``) — standard-var
# matching is exact. setdefault so an outer harness is never clobbered.
os.environ.setdefault("AI_AGENT", "hermes-agent")
os.environ.setdefault("HERMES_AGENT", "true")
# Force UTF-8 stdio on Windows — gateway logs and startup banner would
+5 -2
View File
@@ -11331,11 +11331,14 @@ def _advertise_agent_env() -> None:
``AI_AGENT`` is the emerging cross-agent standard (huggingface_hub's agent
detection reads it; pi and other agents set it — earendil-works/pi#7493)
so generic tooling can attribute subprocesses to the harness that spawned
them. ``HERMES_AGENT`` is the Hermes-specific marker. setdefault: never
them. The value must be our id in the public agent-harness registry
(``hermes-agent`` in huggingface.js ``agent-harnesses.ts``): standard-var
matching is exact, so any other value is counted as "unknown".
``HERMES_AGENT`` is the Hermes-specific marker. setdefault: never
clobber an outer harness (e.g. Hermes running inside another agent's
terminal).
"""
os.environ.setdefault("AI_AGENT", "hermes")
os.environ.setdefault("AI_AGENT", "hermes-agent")
os.environ.setdefault("HERMES_AGENT", "true")
@@ -3,19 +3,35 @@
Port of earendil-works/pi#7493: entry points advertise the agent harness to
child processes via the cross-agent ``AI_AGENT`` standard plus a
Hermes-specific marker, without clobbering an outer harness.
The AI_AGENT value must equal Hermes' id in the public agent-harness
registry (``hermes-agent`` in huggingface.js ``agent-harnesses.ts``) —
standard-var matching there is exact, so any other value is attributed to
"unknown".
The terminal backends additionally export both vars inside every wrapped
shell command (``BaseEnvironment._wrap_command``) so the marker reaches
REMOTE backends (Docker/SSH/Modal/Daytona/Singularity/Vercel) whose exec
environment does not inherit the Hermes process env, and survives the
cross-session leak guard that strips ``HERMES_SESSION_*`` from subprocess
envs in engaged multi-session hosts.
"""
import os
import subprocess
from hermes_cli.main import _advertise_agent_env
# Registry id — must stay in sync with huggingface.js agent-harnesses.ts.
HARNESS_ID = "hermes-agent"
class TestAdvertiseAgentEnv:
def test_sets_both_vars_when_unset(self, monkeypatch):
monkeypatch.delenv("AI_AGENT", raising=False)
monkeypatch.delenv("HERMES_AGENT", raising=False)
_advertise_agent_env()
assert os.environ["AI_AGENT"] == "hermes"
assert os.environ["AI_AGENT"] == HARNESS_ID
assert os.environ["HERMES_AGENT"] == "true"
def test_does_not_clobber_outer_harness(self, monkeypatch):
@@ -30,5 +46,48 @@ class TestAdvertiseAgentEnv:
monkeypatch.delenv("HERMES_AGENT", raising=False)
_advertise_agent_env()
_advertise_agent_env()
assert os.environ["AI_AGENT"] == "hermes"
assert os.environ["AI_AGENT"] == HARNESS_ID
assert os.environ["HERMES_AGENT"] == "true"
class TestWrapCommandAdvertisesHarness:
"""The shell-level export in BaseEnvironment._wrap_command."""
def _wrap(self, command: str) -> str:
from tools.environments.local import LocalEnvironment
env = LocalEnvironment.__new__(LocalEnvironment)
env._snapshot_ready = False
env._session_id = "testsession0"
env._cwd_marker = "__HERMES_CWD_testsession0__"
env._snapshot_path = "/tmp/hermes-snap-testsession0.sh"
env._snapshot_passthrough_names = set()
return env._wrap_command(command, "/tmp")
def test_wrap_command_contains_export(self):
wrapped = self._wrap("true")
assert 'AI_AGENT="${AI_AGENT:-' + HARNESS_ID + '}"' in wrapped
assert 'HERMES_AGENT="${HERMES_AGENT:-true}"' in wrapped
def test_export_precedes_user_command(self):
wrapped = self._wrap("echo payload-sentinel")
assert wrapped.index("AI_AGENT=") < wrapped.index("payload-sentinel")
def test_shell_sets_default_and_preserves_outer(self):
"""Run the wrapped script through real bash both ways."""
wrapped = self._wrap('echo "AI=$AI_AGENT HERMES=$HERMES_AGENT"')
clean_env = {k: v for k, v in os.environ.items()
if k not in ("AI_AGENT", "HERMES_AGENT")}
out = subprocess.run(
["bash", "-c", wrapped], capture_output=True, text=True,
env=clean_env, timeout=30,
)
assert f"AI={HARNESS_ID} HERMES=true" in out.stdout
outer_env = dict(clean_env, AI_AGENT="pi", HERMES_AGENT="false")
out = subprocess.run(
["bash", "-c", wrapped], capture_output=True, text=True,
env=outer_env, timeout=30,
)
assert "AI=pi HERMES=false" in out.stdout
+24
View File
@@ -566,6 +566,13 @@ def _export_dump_excluding_session_vars(
return (
"{ ( "
"unset ${!HERMES_SESSION_*} ${!HERMES_CRON_AUTO_DELIVER_*} "
# AI_AGENT / HERMES_AGENT are per-command attribution markers
# (re-exported by every _wrap_command with outer-harness-preserving
# ${VAR:-default} semantics). Persisting them into the snapshot
# would make the FIRST command's value override a later outer
# harness value arriving via the process env, exactly like the
# session-var leak this dump already guards against.
"AI_AGENT HERMES_AGENT "
f"HERMES_UI_SESSION_ID{extra_unset} 2>/dev/null; "
"export -p; "
") || true; } "
@@ -886,6 +893,23 @@ class BaseEnvironment(ABC):
)
parts.append(f"unset {present} {value}")
# Harness attribution: every tool subprocess advertises that it runs
# under Hermes via the cross-agent ``AI_AGENT`` standard (read by e.g.
# huggingface_hub's agent detection) plus the Hermes-specific
# ``HERMES_AGENT`` marker. The value MUST equal our id in the public
# agent-harness registry (``hermes-agent`` — see huggingface.js
# ``agent-harnesses.ts``); standard-var matching is exact, so any other
# value is reported as "unknown". Setting it here (rather than only in
# the host process env) is what carries the marker into REMOTE backends
# (Docker/SSH/Modal/Daytona/Singularity/Vercel), whose exec env is not
# inherited from the Hermes process. ``${VAR:-default}`` semantics:
# never clobber an outer harness value that arrived via the inherited
# process env (Hermes running inside another agent's terminal).
parts.append(
'export AI_AGENT="${AI_AGENT:-hermes-agent}" '
'HERMES_AGENT="${HERMES_AGENT:-true}"'
)
# Preserve bare ``~`` expansion, but rewrite ``~/...`` through
# ``$HOME`` so suffixes with spaces remain a single shell word.
quoted_cwd = self._quote_cwd_for_cd(cwd)
@@ -864,8 +864,8 @@ Unset the variable or remove it from `.env` to restore normal writes (still subj
| `SESSION_IDLE_MINUTES` | Reset sessions after N minutes of inactivity (default: 1440) |
| `SESSION_RESET_HOUR` | Daily reset hour in 24h format (default: 4 = 4am) |
| `HERMES_SESSION_ID` | **Exported automatically into every tool subprocess** Hermes spawns (`terminal`, `execute_code`, persistent shell, Docker/Singularity backends, delegated subagent runs). Set by the agent to the current session ID; user scripts called from tools can read it to correlate their output, telemetry, or side effects with the originating Hermes session. **You should not set this manually** — overriding it from a parent shell only takes effect outside an agent run, and is overwritten the moment the agent starts a session. |
| `AI_AGENT` | **Set to `hermes` by the CLI and gateway entry points** (only when not already set by an outer harness). The emerging cross-agent standard for child-process attribution — generic tooling (e.g. huggingface_hub's agent detection) reads it to know it runs under an AI agent. Don't set manually. |
| `HERMES_AGENT` | **Set to `true` by the CLI and gateway entry points** so child processes can detect they run inside Hermes specifically. Don't set manually. |
| `AI_AGENT` | **Set to `hermes-agent` by the CLI and gateway entry points** (only when not already set by an outer harness), and exported into every terminal-tool shell — including remote backends (Docker, SSH, Modal, Daytona, Singularity, Vercel). The emerging cross-agent standard for child-process attribution — generic tooling (e.g. huggingface_hub's agent detection) reads it to know it runs under an AI agent. The value matches Hermes' id in the public agent-harness registry. Don't set manually. |
| `HERMES_AGENT` | **Set to `true` by the CLI and gateway entry points** and exported into every terminal-tool shell so child processes can detect they run inside Hermes specifically. Don't set manually. |
## Context Compression (config.yaml only)