A followed Start ended at the first status read where a gateway served the
Webapp on the configured port. The gateway reports its Webapp address by
port alone, so the gateway the restart was replacing passed too when it
already served the Webapp on that port, as an earlier Start's gateway does
when a second Start queued behind it saves another mode or address. The
second Start's record then ended before its restart applied anything, and
the pane enabled Stop on the old config.
Start now notes which gateway process answers before it asks for the
restart (the control socket's identify: pid and start time), and the record
ends only once another process serves the config. The identity is read
before the status, so a gateway replaced between the two reads still counts
as the old one.
Reported by greptile-apps and Adolanium in review of #148632.
The Start record ended when the Start request answered, but the gateway
restart it asked for was still running. A pane reopened in those seconds, or
any other window, showed Restart needed with Restart gateway enabled; a second
window also dropped its 1 s poll and could wait up to 10 s more.
A Start whose restart began is now reported `restarting` past its request,
until a gateway serves the config it wrote, that restart reports a failure, a
Stop or Restart replaces it, or 90 s pass (as long as the pane itself waits).
The restart command can return before the new gateway serves, so the end is
read from the gateway, not from the child's exit.
The Start's own answer carries the record too. The pane keeps that answer as
the status it shows, and where the backend answering is the gateway being
restarted (Desktop's own gateway) every status read fails from the old one's
drain until its replacement serves: a pane reopened then showed the answer's
saved-but-not-restarted config as Restart needed.
Reported by teknium1 and yoniebans in review of #148408.
The port-claim lock lives in the Hermes root's webapp folder, beside the Webapp's credentials. _file_lock created that folder with the umask's mode (0755/0775) and the lock 0644/0664. Create the folder 0700 and the lock 0600 before taking the lock, and close an existing folder other users can reach with the helper the credential writers use. _file_lock is shared by every Hermes lock and takes no mode, so the files are created here first.
plan() chose and saved a standalone profile's webapp.port before checking the Nous sign-in, so a Start refused with "Sign in to your Nous account under Providers first." still wrote the port to config.yaml. Check the sign-in first, so a precondition refusal writes nothing.
_own_port hand-rolled a raw read plus the managed overlay. It now reads
load_user_config_effective(home / "config.yaml"), the loader hermes_cli/AGENTS.md names
for presence-sensitive readers: the user file, ${VAR} expansion and the managed overlay,
with no defaults merged, so a port is still only one the user or the administrator set.
Each standalone profile's gateway has its own change lock, so two first Web Access
starts could both probe the same free port before either saved it, and both kept it.
Web Access now chooses the port and writes it to that profile's webapp.port while
holding a file lock under the shared Hermes root (the auth file-lock helper .env writes
already reuse), before the bundle build and registration. A Start that fails after
that keeps the port as its own.
Web Access read a standalone profile's webapp.port from config.yaml as
written, so a port the managed scope sets read as none. Start then chose
9121 and registered the sign-in callback on it, saving dropped the port
because the managed layer pins it, and the gateway still bound the managed
one: sign-in returned to a port nothing served.
Read the port from the file with the managed overlay on top, as the
gateway loads it, still without the defaults. A managed port is kept,
nothing is written over it, and the callback names it.
A standalone profile that turned on Web Access without its own webapp.port bound 9120, which the default gateway's Webapp holds, so nothing was served and the LAN sign-in callback was registered on the other gateway's address. Start now gives such a profile the first port above 9120 that no other Hermes home claims and nothing listens on, and writes it to that profile's webapp.port, so the gateway binds it, the launcher prints and opens it, the callback names it, and it stays across restarts. The default profile keeps 9120, a webapp.port the user set is never replaced, and with no free port Start refuses and names the setting.
Refs #144648 (WA-13)
After an upgrade the running gateway can still be the old one (a git pull, a service manager that
kept the process). It does not know the Webapp's control commands and answers "unknown verb:
'webapp-status'". hermes webapp, --status and --sign-out printed that protocol text and never named
the fix, and Web Access turned it into "Unavailable", the answer its restart wait stops on.
The Webapp's commands are now registered before the gateway's control socket answers, so an
unknown-verb answer means the gateway predates them. hermes webapp and its --status and --sign-out
say the running gateway is older than the Webapp and to run hermes gateway restart. Web Access
reports such a gateway as serving no Webapp, with that restart as the reason, so the pane offers
its Restart gateway button instead of showing the Webapp as unavailable.
The Web Access status read the webapp: block four times with two validators: the gateway's
webapp_settings, plus its own readers for the port and the public URL, which returned the raw
string. A webapp.public_url the gateway refuses (credentials, a query, a bad port) showed in the
pane as the Webapp's address, and Start in "My own URL" mode accepted URLs the gateway then
refused (a port of 0 or above 65535, an @ in the path, a space), so it registered, saved and
restarted onto a Webapp the gateway kept off.
webapp_settings now reads a present block's valid host, port and public URL whether or not it
turns the Webapp on (an invalid one reads as the default), and the status takes all of them from
that one read. Start checks the public URL with the gateway's webapp.public_url reader and then
requires https on a host other devices reach. The three extra readers are gone. The gateway's
listener is unchanged: a block that is off or unusable still binds main's private listener.
Web Access registered the Webapp's sign-in callback on the standalone dashboard's Portal client
(HERMES_DASHBOARD_OAUTH_CLIENT_ID), saved that id in .env, and `hermes dashboard register` called
back into Web Access so it would register again. If the Portal replaces a client's redirect URI on
update, turning on Web Access broke Nous sign-in on an existing `hermes dashboard` deployment.
Start now registers a client of Web Access's own (named webapp_<adjective>_<noun>) and keeps its
id, its Portal and the callback in webapp/sign-in-registration.json under the gateway home, user
only. It never sends the dashboard's client id and never writes .env. A second Start with the same
callback asks the Portal nothing; a moved callback updates Web Access's client only. While the
gateway serves the Webapp, the Nous sign-in provider uses that client, and a plugin reload keeps
it. The gateway's private listener and the standalone dashboard keep reading
HERMES_DASHBOARD_OAUTH_CLIENT_ID as on main, and a Webapp whose Nous sign-in was set up by hand
keeps the configured client. A home whose record came from an earlier build of Web Access names
the dashboard's client; its next Start registers a client of its own and leaves the dashboard's
alone. `hermes dashboard register` no longer touches Web Access's record.
The desktop guide said Start saves the client id in .env; it now says where the id lives.
Web Access Start registers the Webapp's sign-in callback with the Nous Portal and saves the client
id it gets back as HERMES_DASHBOARD_OAUTH_CLIENT_ID in .env, the same client the standalone
dashboard's Nous sign-in reads. The Desktop guide and the module's docstring said Web Access left
.env alone. Both now say what it writes: the webapp: block of the config, and that client id when it
registers a callback.
With the Webapp on every interface (`webapp.host: 0.0.0.0`) and no `webapp.public_url`, `hermes
webapp` on the host opened the loopback address. Web Access Start registers Nous sign-in for the
LAN address's callback, so a sign-in started there sent the Portal a callback for 127.0.0.1 that it
had never seen, and failed.
For a launch behind the sign-in gate the gateway now opens, in order: `webapp.public_url` when set,
as before; on the LAN, the address in Start's `webapp/sign-in-registration.json`, while that is
still this machine's LAN address and the Webapp's port (a moved address or port would send the
browser elsewhere); otherwise the loopback address, which still serves a password provider and
sign-in set up by hand for it. Native clients keep the loopback address. The docs say so.
Every Web Access start posted the sign-in callback to the Portal's self-hosted-client endpoint
with HERMES_DASHBOARD_OAUTH_CLIENT_ID, the client the standalone dashboard also uses. If the
Portal replaces that client's redirect URI rather than adding one, each repeated post re-points
the dashboard's own OAuth callback at the Webapp, even when nothing about the Webapp changed, and
a Portal outage blocked turning Web Access back on with the same address.
Web Access now records the Portal, client id and callback it registered in the profile's webapp
folder and skips the post when all three are unchanged; a new LAN address or URL, another client
id or another Portal registers again. hermes dashboard register drops that record, because it
updates the same client and may have replaced the Webapp's callback. Whether the Portal replaces
or adds the redirect URI is not settled here.
Start only wrote the webapp: config and restarted the gateway. Nothing built
apps/desktop/dist-webapp except hermes webapp, so on an install where that never ran the pane
reported the Webapp running while every page returned a 404 that named a different product.
Start now builds the bundle with prepare_webapp_renderer, the function hermes webapp uses, which
reuses a current bundle. It runs after the request is checked and before anything lasting: a
failed build registers nothing with the Portal, writes no config, restarts nothing, and its error
names hermes webapp --build-only. The pane waits up to 35 minutes for Start, since that build can
wait 30 minutes for another build of it.
Every Web Access start wrote port 9120 into the webapp block and registered the LAN sign-in
callback on 9120, so a port the user had chosen was silently replaced. A standalone profile with
its own gateway needs a port of its own; after Start it collided with the default gateway's
Webapp on 9120, and while Web Access was off the pane showed 9120 whatever the config said.
Start and Stop no longer write webapp.port. The LAN callback, the status the pane shows and the
addresses built from it all use the configured port, whether or not the Webapp is on.
The status mapped every enabled Webapp whose host was not a wildcard to the "My own URL" mode.
The documented setup for hermes webapp (webapp.enabled: true on loopback) therefore showed as
"My own URL" with an empty link, and the pane offered no way to tell it apart from a Webapp
published through a tunnel.
The mode now follows the config: webapp.public_url means the user's own URL, a wildcard host means
the network, and any other enabled host means this computer only (mode "local"). For a local
Webapp the pane links to the address the gateway serves, says only this computer can open it, and
hides the network choice until it is stopped.
Web Access kept the Webapp's own URL in dashboard.public_url. Stop and a LAN start deleted that
key, an own-URL start overwrote it with the Webapp's address, and every start or stop removed
HERMES_DASHBOARD_PUBLIC_URL from .env. Both belong to the standalone dashboard, which uses them for
its trusted Host check, its OAuth callback and its sign-in gate, so a dashboard published through
a tunnel or reverse proxy stopped working after its next restart.
Web Access now writes only the webapp: block. The own-URL mode stores its address in
webapp.public_url, which the gateway's Webapp listener reads; a LAN start drops it, because the
LAN callback follows each request; Stop changes only webapp.enabled. The pane's address comes from
webapp.public_url too, so it shows what Web Access saved rather than the dashboard's setting.