mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
propagate handler exit codes
Live testing the full wizard against the real v0.39.0 binary
(downloaded + extracted via our own install_iron_proxy()) surfaced
three real bugs that the unit tests couldn't catch:
1. `proxy.http_listens` (plural) — NOT a field in v0.39's config struct.
Our code emitted both `http_listen` (string) and `http_listens`
(list) believing v0.39 accepts both forms. The binary actually
rejects with "field http_listens not found in type config.Proxy"
at YAML unmarshal time, so the daemon fails to start. Confirmed
via strings(1) audit of the v0.39 binary — only `http_listen` is
tagged.
2. `log.audit_path` — NOT a field in v0.39's config.Log struct. Same
class of error: "field audit_path not found in type config.Log".
Per-request audit-log records are not separable from server-level
logs at this binary version.
3. `metrics.listen` defaults to ":9090" — which is the SAME port as
our default `tunnel_port: 9090`. Result: every operator who runs
`hermes egress setup` followed by `hermes egress start` gets
"bind: address already in use" because the proxy listener and the
metrics listener fight for port 9090. We now explicitly pin
`metrics.listen: 127.0.0.1:0` to give it an ephemeral loopback
port that can never collide with tunnel_port regardless of what
operator sets.
Plus a fourth bug — pre-existing but surfaced by the egress live
test — that affects every Hermes subcommand:
4. `hermes_cli/main.py` calls `args.func(args)` at the bottom of
main() but discards the return value. Every subcommand handler
that returns a non-zero exit code (cmd_start refusing because
`fail_on_uncovered_providers=true`, cmd_setup refusing because
--from-bitwarden but BWS unreachable, etc.) was silently exiting 0.
Fix: capture the handler's return value and `sys.exit(rc)` when
it's a non-zero int. Other subcommands' contracts unchanged
because they either return 0/None or don't return at all.
Validation:
- 188/188 in test_iron_proxy.py + test_iron_proxy_cli.py +
test_config.py pass post-fix.
- 5333/5337 in tests/hermes_cli/ pass; the 4 unrelated failures
(test_managed_installs.py + test_update_hangup_protection.py)
are pre-existing on main, not touched by this PR.
- Manual wizard run end-to-end with the v0.39.0 binary in an
isolated HERMES_HOME:
* `egress install` — downloads + SHA-256 verifies + extracts
* `egress setup` — generates CA, mints tokens, writes
proxy.yaml that the binary now accepts (no http_listens,
no audit_path, metrics pinned to 127.0.0.1:0)
* `egress start` — daemon binds 127.0.0.1:9090, listens=yes
* `egress status` — shows pid + listening + mappings
* `egress stop` — clean shutdown, pidfile + nonce removed
* Idempotent re-start returns the running pid without spawning
* curl through the proxy with the openrouter token gets
forwarded; an attacker host gets HTTP 403 (allowlist works);
169.254.169.254 gets HTTP 403 (deny CIDR works)
* Refuse-start paths exit 1 with actionable messages:
- `fail_on_uncovered_providers=true` + ANTHROPIC_API_KEY set
- `credential_source=bitwarden` + BWS_ACCESS_TOKEN unset
* `--rotate-tokens` confirmation gate fires via pty:
typing 'cancel' aborts; typing 'rotate' proceeds and
creates a mappings.json.rotated-<timestamp> backup
Test updates:
- `test_default_bind_is_loopback_not_zero_zero` — asserts the
singular `http_listen` is loopback AND asserts `http_listens`
(plural) is NOT in the rendered yaml.
- `test_default_bind_uses_loopback_on_linux` — replaces
`test_default_bind_includes_docker_bridge_on_linux`. v0.39
only supports one bind per daemon process, so the docker bridge
augmentation is dropped from the rendered config; sandboxes
reach the daemon via host.docker.internal -> host-gateway
mapping, so loopback-only is functional.
- `test_metrics_listener_pinned_to_loopback_ephemeral` — new
regression test asserting `metrics.listen == "127.0.0.1:0"`.
- `test_audit_log_kwarg_does_not_inject_audit_path_v039` —
replaces `test_audit_log_path_lands_in_yaml`. audit_log kwarg
is still accepted for forward compatibility but does NOT emit
log.audit_path until upstream supports it.
|
||
|---|---|---|
| .. | ||
| proxy | ||
| __init__.py | ||
| _parser.py | ||
| _subprocess_compat.py | ||
| auth.py | ||
| auth_commands.py | ||
| azure_detect.py | ||
| backup.py | ||
| banner.py | ||
| browser_connect.py | ||
| bundles.py | ||
| callbacks.py | ||
| checkpoints.py | ||
| claw.py | ||
| cli_output.py | ||
| clipboard.py | ||
| codex_models.py | ||
| codex_runtime_plugin_migration.py | ||
| codex_runtime_switch.py | ||
| colors.py | ||
| commands.py | ||
| completion.py | ||
| config.py | ||
| container_boot.py | ||
| copilot_auth.py | ||
| cron.py | ||
| curator.py | ||
| curses_ui.py | ||
| debug.py | ||
| default_soul.py | ||
| dep_ensure.py | ||
| dingtalk_auth.py | ||
| doctor.py | ||
| dump.py | ||
| env_loader.py | ||
| fallback_cmd.py | ||
| fallback_config.py | ||
| gateway.py | ||
| gateway_windows.py | ||
| goals.py | ||
| hooks.py | ||
| inventory.py | ||
| kanban.py | ||
| kanban_db.py | ||
| kanban_decompose.py | ||
| kanban_diagnostics.py | ||
| kanban_specify.py | ||
| kanban_swarm.py | ||
| logs.py | ||
| main.py | ||
| mcp_config.py | ||
| memory_setup.py | ||
| migrate.py | ||
| model_catalog.py | ||
| model_normalize.py | ||
| model_switch.py | ||
| models.py | ||
| nous_subscription.py | ||
| oneshot.py | ||
| pairing.py | ||
| platforms.py | ||
| plugins.py | ||
| plugins_cmd.py | ||
| portal_cli.py | ||
| profile_describer.py | ||
| profile_distribution.py | ||
| profiles.py | ||
| providers.py | ||
| proxy_cli.py | ||
| pt_input_extras.py | ||
| pty_bridge.py | ||
| relaunch.py | ||
| runtime_provider.py | ||
| secret_prompt.py | ||
| secrets_cli.py | ||
| security_advisories.py | ||
| security_audit.py | ||
| send_cmd.py | ||
| service_manager.py | ||
| session_recap.py | ||
| setup.py | ||
| skills_config.py | ||
| skills_hub.py | ||
| skin_engine.py | ||
| slack_cli.py | ||
| status.py | ||
| stdio.py | ||
| timeouts.py | ||
| tips.py | ||
| tools_config.py | ||
| uninstall.py | ||
| vercel_auth.py | ||
| voice.py | ||
| web_server.py | ||
| webhook.py | ||
| xai_retirement.py | ||