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:
+4
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user