Classic gateway client | Hermes Agent
Skip to main content

Classic gateway client (initial migration)

Normal hermes --cli, hermes chat, hermes chat -q '…' -Q, and top-level hermes -z '…' use the gateway authority. Direct cli.main chat launches also hand off before constructing a local agent. Local bootstrap uses the canonical ensure lifecycle and a single-use private control ticket, not service installation. An explicit HERMES_TUI_GATEWAY_URL remains remote-only: failed connection or authentication never launches a local replacement.

hermes profile create uses the same dial (hermes_cli/profile_provision_gateway.py): a ready owner for the terminal's profile (discovery only, never started for this) or the URL's gateway runs profiles.create in the connection's profile and returns the receipt the CLI prints (report). With no owner, or a local one that predates those fields (-32601 / 4000), the CLI creates in-process; a URL's gateway is never replaced by a local create. A refused create keeps its type (error.data.type); a socket lost after the request was sent, or a reply without a complete receipt, is reported as unknown, never retried. The ~/.local/bin wrapper is written by the CLI, never by the gateway, and only when the profile the gateway made is the directory -p <name> selects on this machine (a loopback URL alone is not enough: a tunnel or container can serve another home).

The client prints the persisted Session: ID to stderr. Resume with hermes --cli chat --resume ID. Input is admitted with a fresh input_id; events and final replies come from the authority. /quit, /exit, /detach, EOF and closing the terminal detach only. /stop sends an execution-generation-fenced interrupt. Pending approvals display /approve ID once|deny|…; clarification uses /answer ID TEXT. The client uses the displayed prompt ID and its captured generation, not a locally reconstructed waiter. /discard ID acknowledges an admission lost across an owner restart (prompt.resolve_unknown). /branch [title], /model <model> [--provider name] and /compress [here [N] | <focus>] [--preview] are revision-fenced session.mutate operations (hermes_cli/gateway_mutations.py); /yolo [on|off] toggles this session's approval bypass on the owner (config.set key yolo). /title [name] (session.mutate rename), /undo [N] and /retry (session.mutate rewind; /undo puts the removed message back in the composer), /new [title] / /reset (session.create with this session's frozen launch request), /usage (the owner's report plus the last committed turn) and /tools (launch toolsets) run in the view (hermes_cli/gateway_chat_commands.py). Every other command goes to the owner as slash.exec: the reviewed reads run there and the rest print /x is not available on the shared gateway yet with a link to the command parity table, which lists what every local client does with every command.

One-shot stdout ends with the final reply. Without -Q, a resumed session first prints its stored history (user: / assistant: lines), and the reply is followed by the Resume this session with: block (see Launch options); -Q prints the reply alone. Exit status is 0 for a completed admission, 1 for failed execution/connection, 2 for unsupported frontend options, 3 for a pending control that requires interactive reattachment or a turn lost in a gateway crash (clear it with hermes sessions discard <id> --yes), and 130 for a keyboard detach. A pending control does not imply cancellation. -z no longer implicitly bypasses approval policy.

Launch options​

The gateway advertises its accepted creation parameters through runtime.describe (session_create.parameters, from gateway.session_policy.CREATE_FIELDS). The client sends a creation option only when it is advertised; a requested option an older gateway does not advertise is refused (Gateway does not support creation options: …). Caller cwd is sent when advertised; an old gateway lacking cwd support prints a warning that it uses its configured execution directory, while explicit --in rejects. A resume may repeat the exact creation flags the session was frozen with (scripts re-run one command line); any flag that would change the frozen route rejects.

Accepted and frozen into the session at creation (_POLICY in hermes_cli/gateway_chat.py): --model, --provider, --base-url, --api-key (memory-only, never persisted), --reasoning, --toolsets, --max-turns, --skills (rendered once into the frozen prompt), --checkpoints, --yolo, --accept-hooks (also HERMES_ACCEPT_HOOKS=1), --pass-session-id, --ignore-rules, --ignore-user-config and --safe-mode (both need an explicit --model; safe mode runs the turn in an isolated worker that never reads the profile). Also accepted: --source <label>, --resume <id-or-title>, -c <title>, -c <title> --create-if-missing, --in <dir>, --query-file, --format stream-json, -z ... --usage-file, bare -c (this terminal's breadcrumb session, else the most recent CLI session), --resume latest (most recent CLI session, this workspace first; both resolved by the owner, session.resume latest='cli') and --list-tools / --list-toolsets (print the catalog and exit; no session). chat -q without -Q ends with main's Resume this session with: hermes --resume <id> block.

Refused with exit 2 (_UNSUPPORTED plus the selection checks in validate_options); the error names where each capability lives now:

OptionWhyUse instead
-w / --worktreeWorktree creation is a launcher-side step the authority does not exposehermes --tui -w
--imageNo client-side attachment staging on this pathattach in hermes --tui or Desktop
--run-budgetNo per-launch run budget in the creation contractagent.run_budget_seconds in config.yaml
-v / --verboseDisplay-only; the client has no verbose rendererhermes logs --follow (the refusal also names hermes chat --tui -v, whose canonical Ink path does not yet carry the flag)
--no-restore-cwdThe gateway keeps the session's frozen cwd--in <dir>
--create-if-missing without -c <name>Nothing to create by name-c <name> --create-if-missing

--compact is not a hermes chat option (argparse rejects it); a direct cli.main hand-off refuses it and names display.compact.

Current parity limits — not full classic CLI parity​

Full legacy presentation, slash registry parity, interactive history editing, auto-reconnect, lost-ACK durable client journals, remote login bootstrap, cold-owner resume and native Windows/macOS QA remain outstanding. The client preserves an explicit remote URL's existing authentication mechanism; it does not acquire or mint remote credentials.

tests/hermes_cli/test_gateway_chat_native.py exercises native tmux/PTY classic fresh input, detach, persisted-ID resume and one-shot through the ordinary daemon with a loopback model, isolated HOME/HERMES_HOME and caller cwd. Constructor instrumentation records no client AIAgent, SessionDB or GatewayRunner. The test also denies an invalid approval choice, then reconnects and consents before a real terminal effect on an owned fixture. Stale-control/active-Stop native coverage and native clarify interaction remain separate acceptance work.

For two-checkout compatibility testing, load the narrowly scoped pytest option plugin and specify the disposable gateway peer's checkout (the normal runner's hermetic environment deliberately removes arbitrary environment overrides):

scripts/run_tests.sh -j 1 --file-retries 0 tests/hermes_cli/test_gateway_chat_native.py \
-p tests.hermes_cli.gateway_client_options --client-test-runtime-root=/path/to/runtime-checkout

The receipt explicitly reports caller_cwd_effect_verified. On a cwd-capable runtime the native client submits model/toolsets/cwd and the real terminal writes its actual execution directory into an owned temporary receipt. On an older runtime, explicit --in must reject without that effect. Do not count the latter as cwd parity. Both client and daemon use isolated test homes; this option does not install a service or alter the selected checkout.