Review follow-ups: `providers.<id>.models.<model>.timeout_seconds` now
matches (a second <...> segment no longer ends the pattern), the YAML
warning filter is scoped with catch_warnings, and the slash-command list
drops a `hidden` filter CommandDef never had.
DEFAULT_CONFIG changed 183 times in 30 days and nothing checked that a new
key reached the docs; /pause (the global emergency stop) went undocumented
for two months.
scripts/ci/check_docs_drift.py lists every DEFAULT_CONFIG leaf and every
registered slash command and finds the ones the docs never mention. A key is
documented by its dotted path, a placeholder path in inline code
(`auxiliary.<task>.timeout`, `display.pet.*`), or a YAML example nesting it
under its parents; a slash command by a `/name` row in
reference/slash-commands.md.
The 132 keys undocumented today are baselined in docs_drift_baseline.json;
tests/ci/test_docs_drift.py fails only on NEW gaps, so it never blocks an
unrelated PR, and documenting a baselined key never fails. Deterministic,
stdlib + repo imports, ~1s.
/pause is documented here, so the slash-command baseline starts empty.
contributing.md says what to do when the test fails.
The chat notice's Rename link carries the account id; a link whose account is
gone opens nothing and says so. A docked Capabilities page takes each new
link's scope, and of two mounted pages only one acts on a link. A started add
or reconnect refreshes the account list at once.
Two quick clicks could reach the backend's reissue before either mint
finished and start two replacement sign-ins. The button now follows the
app's pending connect.
- Reconnect addresses the account by its connection_id, never by name. An
optimistic rename changes the row's name before the server answers, and
another session may hold that name, so a name could repair a different
account. The name stays display-only.
- An open operation whose targets all failed or expired lets its own
account retry: that row's Reconnect stays enabled and the sign-in
summary shows Try again, which resends the operation's own request.
Every other account stays blocked until the operation ends.
- The name editor keeps the value the gateway refused with its error and
shows the error while the field still holds that value, so a refused
add explains why instead of hiding the reason once the user has typed.
A row's Reconnect on an account with no name sent no address, so the gateway
had nothing to repair. It now sends the account's connection_id, which
connectors.connect accepts with reconnect for exactly this case. Named rows
still go by name; an app-level reconnect is unchanged.
Review round 3: on a multi-connection desktop the scope selector pins
`{connectionId, profile}` while an operation started from the ambient
scope holds the bare profile name, so re-picking the same profile hid
the open sign-in. Scopes now compare by the backend they resolve to
(connection, else the active one, plus profile).
Review round 2: the open-sign-in lookup matched the app only, so a
Gmail sign-in left open in one profile disabled Add another account and
Reconnect for Gmail in every other profile, and the stop-waiting path
could act on the other profile's operation. The lookup now matches the
profile scope the operation was started in.
Review round 1:
- An older rename reply or rollback no longer overwrites a newer
rename of the same account, and a refused rename reopens its editor
only when the user has not started another edit.
- Add another account and per-row Reconnect wait while a sign-in for
that app is open, so the sign-in controls and the browser always
speak for the same account.
- Reconnect on an unnamed row stays unnamed instead of borrowing the
summary account's name.
- The chat's Rename link carries the chat's profile and connection, and
a pending rename belongs to its app and clears when the dialog closes.
- The account list scrolls inside the dialog instead of clipping.
The account strings grew en/fr/de/es/zh/types past the per-file line
cap the health ratchet holds them to. The connector card and account
strings now live in their own topic files (`<locale>_connector_card.ts`,
`<locale>_connector_accounts.ts`, `types_connectors.ts`), the same split
the other large topics already use, which leaves each catalog shorter
than on main. Also regenerates `locales/_keys.desktop.json` for the new
keys.
A hosted connector can now hold several accounts (gmail "home" and
"work"). The connector dialog lists every live account by its name, or
the vendor label when it has none, with Rename, Remove (confirmed, that
one account only) and Reconnect on the rows that need it. Reconnect
names the account it repairs, so nothing is deleted first. "Add another
account" asks for a name and connects under it. Accounts a reconnect
replaced fold under "N retired". The card shows the name, or "N
accounts". Rename paints at once and rolls back with the gateway's
reason when it is refused.
When the agent connects an account under a name it picked, the chat
card says "Hermes named this Gmail account work." once the account is
connected. Rename opens that account's name editor on the Connectors
tab; nothing opens by itself.
The letter shortcuts that act on the selected row map to handlers, and the
reconnect refusal and hint choice live beside the other account rules, so
the panel stays under the complexity target.
The /connectors panel offers c only on an account that needs repair; on a
healthy row the hint drops it and the key answers that the account is
connected. connect --reconnect on an active account says so and starts
nothing unless --force. A failed or expired sign-in shows the gateway's
detail. The docs say account is present only for a named account.
Ink reports Ctrl+C as 'c' with key.ctrl set, so the panel's c shortcut
fired Reconnect on the same press the global handler used to close the
panel. Letter shortcuts now go through panelShortcut, which ignores
Ctrl and Meta chords.
Review on ee2d14de98:
- `hermes connectors` registers in _build_cli_parser next to `mcp`, not
from inside the mcp builder. The sessions lazy handler moves into its
own subcommands module to keep main.py under its size cap.
- `disconnect --alias` can pick a retired account (shown by
`accounts --all`); reconnect still never targets one.
- The connect link and its instruction flush at once, so a piped run
shows the link before the wait.
- The two reference links in the tool-gateway guide go up two levels.
The agent core now takes `connection_id` on a reconnect and marks retired
accounts `disabled`. Both surfaces use them:
- /connectors `c` and `hermes connectors connect <app> --reconnect --alias
<name-or-label>` repair that exact account, named or unnamed. The old
account ends retired and the replacement takes its place; nothing is
deleted. The "name it first" refusal for unnamed rows is gone.
- Retired accounts fold behind Tab in /connectors and behind --all in the
CLI, and cannot be picked for a reconnect.
- The accounts panel draws nothing while a prompt is open: the shared
remove confirm renders under the floating panel and was hidden by it.
- The remove confirm title drops its own "y/N"; the prompt shows the keys.
Review round 2: drop the pass-through request helper, and when the link
deadline passes, say only that waiting ended. A late reply may still
have connected the account, and the reloaded list shows its real state.
Review round 1:
- Remove now asks through the shared confirm prompt, and the overlay stops
reading keys while any agent question or confirm is open, so a `y` meant
for another prompt can no longer remove an account.
- Reconnect needs the account's name: an unnamed row would send an app-only
request that reports success for any healthy account of that app. The row
asks the user to name it first.
- The link wait ends at the operation's deadline and on any coded refusal
(access lost, signed out), showing the error instead of waiting forever.
- Esc on a link reloads the list, so the new pending account shows.
The connectors parser registers from the mcp builder, next to the command
it sits beside in --help, so main.py does not grow. $isBlocked reads a key
list instead of a 21-term boolean, and the accounts overlay's app picker
and link view are their own components.
A /connectors overlay lists every account as app, name and status, with
unnamed accounts shown by their app label and retired ones folded behind
Tab. r renames inline, a adds another named account and waits on its link,
c reconnects one account, x removes one after a confirmation.
When the agent connects an account under a name it chose, the setup card
title carries that name and the settled transcript line tells the user
"Hermes named this <app> account <name>. Rename it anytime."
Enter on a fully typed TUI-local command now runs it: the gateway does
not know those commands, so its fuzzy completion could rank another one
first (typing /connectors ran /login).
Covers account names, no default account, the agent telling the user the
name it chose, connector_alias on tools and account on results, rename,
reconnect keeping the old account retired, and where to manage accounts
(desktop, TUI /connectors, hermes connectors, Nous Portal).
One app can now hold several named accounts. The CLI lists them (retired
ones folded behind --all), connects a new named account and waits for the
browser step, renames by name or app label, and removes one account after
a confirmation. Ctrl-C settles the open operation so the next connect of
that app is not blocked.
An id-based retry omits alias by contract, but the guard compared the
open target's alias with the request's, so retrying a named account by
its id answered UNKNOWN_TARGET. Aliases are compared only when the
request carries one.
Three invariants, each red before the fix: a healthy unnamed account
reconnected by label mints nothing unless forced (then by id); an open
repair of one account is not reused for another; a session retry with
another account's id is refused. The RPC helper now reads only the
current call's reply from a shared transport.
Review findings on the id-based repair:
- an open repair of one unnamed account was reused for another, because
the operation lookup matched only connector and alias; it now matches
the repaired account id too
- a session retry carrying another account's id re-minted the open
target; it is refused like a retry naming another alias
- reconnect by label restarted a healthy unnamed account; only a repair
a surface asked for by id skips the healthy check
- reconnect by label with force sent the label as an alias; the label
now resolves to the account id first
manage_connections reconnect could only address an account by alias, so
an unnamed account (addressed by its label, as rename already allows)
failed the alias format check and the only way out was a forced
reconnect that minted a second account beside the broken one. A label
now resolves to that account and the repair sends its id. status marks
retired accounts. A CONNECTION_REQUIRED result keeps the account id of
a link the user already holds, so the model can tell a waiting sign-in
from a new one.
The gateway marks the account a reconnect replaced as disabled; Hermes
dropped the field, so every surface showed a retired account as an
ordinary inactive one. Account rows now carry it. connectors.connect
takes connection_id with reconnect, the only way a surface can repair
an unnamed account; operation targets carry it as repair_id.
A retry from the connection card re-minted with the target's alias, not
the account id the first repair recorded. If that account was renamed and
its old name given to another login while the card stayed open, Try again
restarted the other login. A repair with no explicit id now reuses the
named target's recorded connection id.
A reconnect naming an account sent only its alias, so the gateway had to
look the account up by name again. The account list the repair check
already reads carries the account's connection id; the connect request
now sends that id (and no alias) so the gateway restarts exactly that
account. A name no account has yet still goes out as an alias.
A rename through the real portal client sends PATCH with the new name
and returns the renamed row. A failed account read mints nothing on
reconnect, and a session retry for another account is refused. Both fix
tests are red with the fixes reverted.
connectors.connect with an alias reissued the session's open target even
when that target is a different account of the same connector, handing
back the wrong account's login link. A retry with an alias must match
the open target's alias; one without an alias keeps today's behaviour.
reconnect with an alias read the account list from Nous Portal and
treated a failed read as 'not connected', so a portal outage sent the
user through a new login for a healthy account. The read error now
reaches the tool result and nothing is minted.
Invariants, each red on main: the account a call ran under survives the
single and batch dispatch paths; connect with an alias asks for that
account; reconnect with an alias repairs it while another account is
healthy; rename of an unknown name lists the valid ones; an open
operation is not reused for another account of the same connector; the
connect and rename RPCs carry the alias and map a taken name.
The portal client wraps transport and parse failures in ToolGatewayError,
so the soft account reads in status and reconnect catch that, not every
exception.
connectors.connect takes an alias (one connector per call), operation
targets carry their alias, and connectors.accounts.rename names or
renames one account, answering ALIAS_TAKEN when the name is in use.
Generated TypeScript contracts regenerated.
connect sends the alias so the gateway adds or reuses that account.
reconnect with an alias checks that account's own row instead of the
connector-wide connected flag, which reports any account. rename resolves
the alias (or the label of an unnamed account) to its id and renames it
through Nous Portal; an unknown name lists the valid ones. status lists
each connector's accounts and stays usable when that read fails.
Target carries the account alias and shows it in its snapshot. An open
operation is reused only for the same connector AND alias, so connecting
gmail 'work' never joins an open operation for the default gmail account.
A hosted target object takes 'alias' (the account's name) and 'to' (a
rename's new name), and the tool gains a 'rename' action. Targets parse
into HostedTarget; a call with an alias carries that one target, and MCP
targets refuse both fields.
The gateway now reports the alias of the account each call used. Keep it
through the wire model, the result dict, both dispatch paths and the
model-facing entry, and show the chosen alias in the tool label.
lefthook's `root: web/` is a string-prefix filter, so `website/sidebars.ts`
matched it, was rewritten to `.site/sidebars.ts`, and eslint aborted the
commit ("No files matching the pattern"). Every docs PR that registers a
page in the sidebar hit it. Anchoring the glob to `web/**` keeps web/
files linted and leaves website/ alone.
Replaces the catalog stub with a vendored, Hermes-specific port of
wheresryan22/anatomy pinned at dbf697a (v1.0.0). The skill explains a
technical idea by building it as an interactive isometric machine
figure (SVG, optional WebGL layers, beta 3D mode) and proves the
drawing with a geometry audit and a line-end checker.
Vendored verbatim: kit/, references/, scripts/, the example sources,
LICENSE. Hermes changes:
- SKILL.md gains a "Running it in Hermes" section: ${HERMES_SKILL_DIR},
build in a copied workspace (build.mjs writes next to itself, so it
must never run inside the installed skill), vision_analyze for the
PNG reviews, background terminal for the long audits, clarify for
the Epic scope call, how to deliver results on CLI and messaging.
- The upstream body is kept, with "read every PNG" mapped to
vision_analyze and the background runs mapped to the terminal tool.
- Example sources only (528 KB): the 9 MB of built HTML pages are
dropped and the skill says how to rebuild them in seconds.
- capture/drive/turn-check now find the Playwright headless shell that
Hermes' browser tool installs (Linux and macOS cache dirs) before a
system Chrome, and pass --disable-background-networking. Under
strace the headless shell makes zero outbound connections; a desktop
Chrome reaches Google services even with that flag.
- The desk-computer example's CRT shows an H mark and types
"hello, hermes" instead of another vendor's mark and name.
- Two CC-22 functions in the vendored spot-plate example carry a
reviewed health allow rather than a refactor, so the port stays
diffable against upstream.
.gitignore: the bare `examples/` pattern (meant for dataset dumps)
swallowed skill example dirs, which other skills had to force-add.
Skill trees are now allowlisted.
test_caution_plugin_accepted_via_callback parametrized two desktop/plugin.js
fixtures (dig lookup, authorized_keys help string) that this same PR correctly
demotes to safe, so the caution-accepted path no longer triggered and the test
failed. Replace them with a /proc/self/fd read (proc_access, still high): a
genuine caution shape the sweep confirms stays flagged (hermes-trove). The
eval('1+1') case is unchanged.