A same-user gateway whose control pipe or process refuses the client was
reported as `Gateway inaccessible (authorization)`, which reads as a
credential failure. Two real causes hide behind it:
- #139845: the gateway runs elevated; the non-elevated Desktop's OpenProcess
on its PID fails (err 5).
- #140252: a session-0 S4U boot-task gateway won the control pipe; the
interactive Desktop can never reach it.
discover/ensure now probe the owner PID (gateway.pid / gateway.lock) on a
refusal: ProcessIdToSessionId for both sides, TokenElevation for both. A
different session is `gateway_other_session`; a same-session owner that is
elevated (or whose token a filtered client cannot open) is `gateway_elevated`.
Each carries a remedy in `detail`, which Desktop already shows in its boot
error. Anything else keeps `authorization`. Exit code stays 4.
No Windows host was available: the classifier and mapping are unit-tested,
the native probes are covered by tests/gateway/test_refused_owner_windows.py
(Windows CI); the elevated-owner case needs a live retest.
Refs #134889 (W8)
Refs #139845
Refs #140252
Refs #140337
What: `gateway ensure --json` now publishes the owner's boot commit
(`endpoint.code_sha`, from its control-socket `identify`) and the ensure
client's own commit (`client_code_sha`, same `get_code_identity` resolver).
Desktop compares them in ensureLocalGateway: a ready owner that provably runs
other code is restarted once with a supervised `hermes --profile P gateway
restart` (P = the profile owning the process: `default` for a served
secondary), then re-ensured. The Models-page "Restart Hermes"
(hermes:backend:recycle) runs the same supervised restart for a local
descriptor before dropping it. Both reuse runGatewayEnsure (resolver, spawn
env scrub, Windows cmd.exe quoting) with a 120 s bounded client.
Why: main's 55b51fa8ad8..61b69853eec made Desktop attach only to a backend
booted from its checkout. The attach model dropped that, so after
`hermes update` the running gateway answered model calls 503 "Restart
required", and Restart only tore down processes Desktop owns (none) — the next
ensure re-attached the same stale gateway forever.
Guards: unknown identity on either side (non-git install, older gateway, older
CLI) keeps the plain attach. One restart per (gateway, client) commit pair per
process, shared by concurrent dials; an owner a restart cannot move (a unit
pinned to another checkout) is then attached as-is rather than restarted on
every dial. A failed restart is logged; the re-ensure reports what answers.
Red: local-gateway.test.ts 'a ready owner running other code than this Hermes
is restarted once, then re-ensured' (pre-fix: 1 failed | 13 passed);
backend-recycle.test.ts 'kills the owned SSH serve before the primary child…'
and 'recycles a pooled profile…' (2 failed | 3 passed);
test_gateway_runtime_home_identity.py::test_ready_endpoint_carries_the_owner_boot_commit
(AttributeError: no code_sha). Green: vitest 20/20 (local-gateway,
backend-recycle, local-gateway-python); pytest gateway_runtime* 16 passed,
3 skipped. Live (fake HOME): ensure payload carries matching code_sha /
client_code_sha; `gateway restart` on an unsupervised owner drained it and
the next ensure attached a fresh instance_id.
(cherry picked from commit d8c5356625168e582c5c2ebc64f4950ecf7e0972)