Files
kshitijk4poor 6e9f696ee6 refactor(profiles): one create receipt type; config outages and 4064 read as unknown
Round-3 review of the gateway create path found three defects and one
structural duplication.

- A config backend outage mid-create read as "nothing was created".
  ConfigBackendUnavailable is a SystemExit, so it escaped the handler's
  `except Exception` and the dispatcher answered 4064, which the client
  read as refused-before-create, even when the profile had already been
  published. The handler now catches it (and builds the receipt inside
  the same try) and answers 5062. The client no longer treats a bare
  4064 as "nothing was created"; only 4061 (no name) is.
- Every `hermes profile create` loaded gateway.platforms (~225 modules,
  about +105 ms median) even with no gateway URL, because the
  safe_proxy_url_for_log import sat above _where's early return. It now
  loads only when a URL is set; a test pins that a URL-less create loads
  neither gateway.platforms nor pydantic.
- A newer gateway that added a key under `mirrored` turned a successful
  create into "may have created": the strict check forbade extra keys
  there. `mirrored` is now a mapping of known value types, so new keys
  are kept and never printed.

The receipt had two definitions: the CreateReceipt dataclass and the
contract's ProfileCreateReceipt pydantic model, kept in step by a
field-name test and a hand-written check that skills_seeded["copied"] was
a list. There is now one: the stdlib dataclass ProfileCreateReceipt in
hermes_cli.profile_provision, which the contract uses directly as
ProfilesCreateResult.receipt (pydantic validates dataclasses), so the
generated TS/OpenRPC still describe it. from_wire validates it strictly
with a TypeAdapter (pydantic only on the gateway path). The receipt
carries what the CLI prints, skills_synced (a count) and
skills_opted_out, instead of the raw sync-result dict, which removes the
`copied` check; RECEIPT_FIELDS and the parity test are gone.

Also:
- The refusal chunk limits are imported from gateway_client
  (ERROR_DATA_TEXT / ERROR_DATA_ITEMS, now named there) instead of
  copied, so lowering the client cap can't silently drop every part.
- A typed refusal with no text is still a refusal ("nothing was
  created"), not "may have created".
- POST /api/profiles uses CREATE_REFUSALS.
- The wrapper check reuses profiles.profile_matches_home.
- The developer guide states the wrapper rule the code enforces.
2026-11-12 01:17:05 +05:30

8.2 KiB

title, description
title description
Classic gateway client How hermes chat / --cli attach to the gateway authority

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:

Option Why Use instead
-w / --worktree Worktree creation is a launcher-side step the authority does not expose hermes --tui -w
--image No client-side attachment staging on this path attach in hermes --tui or Desktop
--run-budget No per-launch run budget in the creation contract agent.run_budget_seconds in config.yaml
-v / --verbose Display-only; the client has no verbose renderer hermes logs --follow (the refusal also names hermes chat --tui -v, whose canonical Ink path does not yet carry the flag)
--no-restore-cwd The 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.