Skip to content

feat(hermes): add Hermes Agent harness bundle#320

Closed
ptone wants to merge 4 commits into
mainfrom
scion/hermes-harness
Closed

feat(hermes): add Hermes Agent harness bundle#320
ptone wants to merge 4 commits into
mainfrom
scion/hermes-harness

Conversation

@ptone

@ptone ptone commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds the complete Hermes Agent harness bundle at harnesses/hermes/, following the same pattern as the existing codex, opencode, and antigravity harnesses
  • Hermes Agent is Nous Research's MIT-licensed AI coding agent (pip install hermes-agent)
  • Supports API key auth for Anthropic, OpenAI, and Google AI Studio (no auth-file, oauth, or Vertex AI)

Files

File Purpose
config.yaml Harness config: model aliases, command flags (hermes chat --yolo -q), capabilities, auth types
provision.py Container-side provisioner: API key → ~/.hermes/.env, instruction projection → AGENTS.md, MCP → ~/.hermes/mcp.json, env overlay
Dockerfile Installs Node.js 22, ripgrep, and hermes-agent on scion-base
cloudbuild.yaml Multi-platform Cloud Build for scion-hermes image
capture_auth.py Post-auth credential capture for no-auth flow
README.md Bundle documentation

Design decisions

  • Auth precedence: ANTHROPIC_API_KEY > OPENAI_API_KEY > GOOGLE_API_KEY (Anthropic first since Hermes defaults to Claude)
  • No telemetry: Hermes has Langfuse but no native OTEL, so telemetry support is declared no
  • No TOML editing: Unlike codex, Hermes uses env vars and .env files for config, keeping provision.py much simpler
  • MCP format: Hermes reads ~/.hermes/mcp.json with mcpServers key, similar to the universal schema

Test plan

  • Verify scion harness-config install harnesses/hermes succeeds
  • Verify provision.py handles all auth key combinations correctly
  • Verify MCP server translation produces valid Hermes mcp.json
  • Build Docker image with docker build --build-arg BASE_IMAGE=scion-base:latest -t scion-hermes:latest -f Dockerfile .
  • Run Hermes agent with API key and confirm it starts correctly

Scion Agent (hh-dev) added 3 commits June 27, 2026 16:59
Add the Hermes Agent harness bundle scaffold with:
- config.yaml: harness configuration with API key auth (Anthropic,
  OpenAI, Google AI Studio), model aliases, command flags, and
  capability declarations
- Dockerfile: scion-base overlay installing Node.js 22, ripgrep,
  and hermes-agent via pip
- cloudbuild.yaml: multi-platform Cloud Build config for scion-hermes
Minimal provision.py handling:
- API key auth with ANTHROPIC > OPENAI > GOOGLE precedence,
  written to ~/.hermes/.env
- Instruction projection into AGENTS.md with scion-managed blocks
- MCP server config written to ~/.hermes/mcp.json
- Env overlay with HERMES_YOLO_MODE, HERMES_QUIET, HERMES_ACCEPT_HOOKS,
  and optional HERMES_INFERENCE_MODEL from model alias resolution
- capture_auth.py: credential capture for no-auth flow, reads from
  inputs/capture-auth-config.json and stores via sciontool
- README.md: bundle documentation with install, auth modes, and build
  instructions

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new harness bundle for the Hermes Agent, featuring container-side provisioning, credential capturing, and MCP server configuration. The code review identifies several critical improvements: fixing a bug in provision.py that breaks the no_auth flow when auth-candidates.json is empty, defining required_files in config.yaml to enable capture_auth.py to function, avoiding a dummy pip install failure in the Dockerfile when using the default 'latest' version, adding type-safety checks to prevent AttributeError crashes when parsing JSON inputs, and refactoring provision.py to use the defined HERMES_ENV_FILE constant.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +410 to +419
if not candidates and no_auth_behavior:
print(f"hermes provision: no-auth mode (behavior={no_auth_behavior}), skipping auth setup", file=sys.stderr)
method = "none"
env_key = ""
else:
try:
method, env_key = _select_auth_key(explicit, env_keys)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return EXIT_ERROR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If auth-candidates.json is present but contains no valid keys (which is the standard way no-auth is represented when the staging layer runs but finds no secrets), candidates is a non-empty dictionary (containing schema_version: 1 etc.). Thus, not candidates evaluates to False, and the script proceeds to the else block where _select_auth_key raises a ValueError because env_keys is empty. This completely breaks the no_auth behavior when auth-candidates.json is present but empty of keys. We should check if any supported key is present in env_keys instead of checking not candidates.

Suggested change
if not candidates and no_auth_behavior:
print(f"hermes provision: no-auth mode (behavior={no_auth_behavior}), skipping auth setup", file=sys.stderr)
method = "none"
env_key = ""
else:
try:
method, env_key = _select_auth_key(explicit, env_keys)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return EXIT_ERROR
has_supported_key = any(k in env_keys for k in AUTH_KEY_PRECEDENCE)
if not has_supported_key and no_auth_behavior:
print(f"hermes provision: no-auth mode (behavior={no_auth_behavior}), skipping auth setup", file=sys.stderr)
method = "none"
env_key = ""
else:
try:
method, env_key = _select_auth_key(explicit, env_keys)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return EXIT_ERROR

Comment on lines +75 to +80
auth:
default_type: api-key
types:
api-key:
required_env:
- any_of: ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The no_auth.message instructs the user to run capture_auth.py to capture credentials after running hermes setup. However, capture_auth.py relies on inputs/capture-auth-config.json, which is only generated if there are required_files defined under auth.types in config.yaml. Since there are no required_files defined here, capture-auth-config.json will never be generated, and capture_auth.py will always exit with EXIT_NO_CREDS (2) without capturing anything. To fix this, you should define the required_files (e.g., ~/.hermes/.env or whichever file hermes setup writes credentials to) under the api-key auth type.

auth:
  default_type: api-key
  types:
    api-key:
      required_env:
        - any_of: ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
      required_files:
        - name: HERMES_ENV
          target_suffix: .hermes/.env

Comment thread harnesses/hermes/Dockerfile Outdated
Comment on lines +31 to +32
RUN pip install --no-cache-dir "hermes-agent==${HERMES_VERSION}" \
|| pip install --no-cache-dir hermes-agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If HERMES_VERSION is latest (the default), running pip install "hermes-agent==latest" will fail and print an error in the build logs before falling back to the unpinned install. We can avoid this dummy failure by checking if the version is latest first.

RUN if [ "${HERMES_VERSION}" = "latest" ]; then \
      pip install --no-cache-dir hermes-agent; \
    else \
      pip install --no-cache-dir "hermes-agent==${HERMES_VERSION}"; \
    fi

Comment on lines +58 to +63
with open(config_path, "r", encoding="utf-8") as f:
try:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return []
creds = data.get("credentials")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If capture-auth-config.json is malformed or contains a non-object (like a list or string), json.load(f) will succeed but data won't be a dictionary, causing data.get to raise an AttributeError. We should verify isinstance(data, dict) before calling .get().

Suggested change
with open(config_path, "r", encoding="utf-8") as f:
try:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return []
creds = data.get("credentials")
with open(config_path, "r", encoding="utf-8") as f:
try:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return []
if not isinstance(data, dict):
return []
creds = data.get("credentials")

Comment on lines +395 to +400
if os.path.isfile(auth_candidates_path):
try:
candidates = _load_json(auth_candidates_path) or {}
except (OSError, json.JSONDecodeError) as exc:
print(f"hermes provision: invalid auth-candidates.json: {exc}", file=sys.stderr)
return EXIT_ERROR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If auth-candidates.json is not a dictionary (e.g., it's a list or other JSON type), candidates.get() on line 402 will raise an AttributeError and crash the provisioner. We should verify isinstance(candidates, dict) after loading.

Suggested change
if os.path.isfile(auth_candidates_path):
try:
candidates = _load_json(auth_candidates_path) or {}
except (OSError, json.JSONDecodeError) as exc:
print(f"hermes provision: invalid auth-candidates.json: {exc}", file=sys.stderr)
return EXIT_ERROR
if os.path.isfile(auth_candidates_path):
try:
candidates = _load_json(auth_candidates_path) or {}
except (OSError, json.JSONDecodeError) as exc:
print(f"hermes provision: invalid auth-candidates.json: {exc}", file=sys.stderr)
return EXIT_ERROR
if not isinstance(candidates, dict):
print("hermes provision: auth-candidates.json is not a dictionary", file=sys.stderr)
return EXIT_ERROR

Comment on lines +136 to +146
def _write_hermes_env(env_vars: dict[str, str]) -> None:
"""Write key=value pairs to ~/.hermes/.env."""
hermes_dir = _expand("~/.hermes")
os.makedirs(hermes_dir, exist_ok=True)
target = os.path.join(hermes_dir, ".env")
tmp = target + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
for k, v in sorted(env_vars.items()):
f.write(f"{k}={v}\n")
os.chmod(tmp, 0o600)
os.replace(tmp, target)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The constant HERMES_ENV_FILE is defined on line 56 but never used. Instead, the path ~/.hermes/.env is hardcoded and duplicated here. We should use the HERMES_ENV_FILE constant to improve maintainability.

Suggested change
def _write_hermes_env(env_vars: dict[str, str]) -> None:
"""Write key=value pairs to ~/.hermes/.env."""
hermes_dir = _expand("~/.hermes")
os.makedirs(hermes_dir, exist_ok=True)
target = os.path.join(hermes_dir, ".env")
tmp = target + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
for k, v in sorted(env_vars.items()):
f.write(f"{k}={v}\n")
os.chmod(tmp, 0o600)
os.replace(tmp, target)
def _write_hermes_env(env_vars: dict[str, str]) -> None:
"""Write key=value pairs to ~/.hermes/.env."""
target = _expand(HERMES_ENV_FILE)
os.makedirs(os.path.dirname(target), exist_ok=True)
tmp = target + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
for k, v in sorted(env_vars.items()):
f.write(f"{k}={v}\n")
os.chmod(tmp, 0o600)
os.replace(tmp, target)

- [H1] Add provision_test.py with 16 tests covering auth resolution
  (ANTHROPIC>OPENAI>GOOGLE precedence), instruction projection (compose,
  stale-block cleanup, file removal, malformed-marker safety), and MCP
  entry building (stdio, sse, streamable-http, unknown transport)
- [M1] Add HERMES_HOME=/home/scion/.hermes to env overlay
- [M2] Create .hermes/skills directory in Dockerfile
- [M3] Set 0600 permissions on mcp.json to protect auth headers
- [M4] Clean up pip install fallback with empty default ARG
- [L1] Remove dead HERMES_ENV_FILE constant
@ptone ptone closed this Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant