d871cda170e140c227ac437036004a0fe19061b4
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
edfe4f5136 |
refactor(honcho): dedupe refresh-failure handling; harden exchange budget, dogpile cooldown, and rebuild race
Follow-ups from review of #80590: - oauth.py: extract _rotate_and_persist() — the twin ~18-line OAuthRefreshError permanent/transient handling blocks in ensure_fresh_token and force_refresh_token were byte-identical except the log verb. - oauth.py: cap the exchange cycle at _REFRESH_TOTAL_BUDGET_SECONDS (20s). The retry runs while holding the global refresh locks on the path to a memory call; a timed-out first attempt no longer earns a second full 15s exchange (~32s lock hold -> <=20s). - oauth.py: transient-failure cooldown (_refresh_failure_at, 30s). Waiting threads and later turns fail open to the stale token instead of serializing their own full exchange cycles against an endpoint that just failed. Cleared on successful rotation and re-login. - oauth.py: mtime-gate reauth_required()'s config read — the dead-grant state persists until re-login, and the verdict can only change when the config file is rewritten; drop the per-call read+parse. - oauth.py: derive _TOKEN_VALUE_RE from ACCESS_TOKEN_PREFIX / REFRESH_TOKEN_PREFIX so a prefix change can't silently break redaction; promote redact_tokens to public (session.py imported the private name). - session.py: fast path in _reauth_required — skip config-path resolution entirely while no grant is dead (runs before every SDK call). - session.py: client-generation counter closes the fetch/store race in _sdk_session/_get_or_create_peer — an object resolved from the old client mid-rebuild is no longer cached (it would 401 forever and burn a token rotation per retry). - __init__.py: drop the getattr/callable/except triple-guard in _pop_auth_notice; the manager is always None or HonchoSessionManager. 7 new tests (budget, cooldown x3, generation guard, fast path); all mutation-checked (disabling each guard fails its test). honcho_plugin 293 passed; plugins/memory 285 passed; live E2E against a real HTTP token endpoint re-verified. |
||
|
|
da1f8779ef | style(honcho): trim auth recovery comments to one line each | ||
|
|
6ea01262fc |
fix(honcho): recover memory from mid-session oauth 401s and tell the user once
An expired access token could pause Honcho memory for hours with no user-facing signal: ensure_fresh_token swallowed every exchange failure and returned the stale token, no code handled a 401 from the Honcho API, and each failed dialectic cycle widened the cadence backoff. Hypothesis for the trigger (not confirmed): the refresh POST times out after the server already rotated the token pair, Hermes keeps the old refresh token, and the eventual replay lands outside the server's 60-second rotation grace window, which revokes the whole grant. - oauth: the exchange reads the token endpoint's error body instead of discarding it. invalid_grant and other permanent OAuth errors mark the grant dead so no code retries a revoked grant; transient failures retry once immediately, which keeps a replayed refresh token inside the grace window. Log lines redact token values. - oauth: force_refresh_token() rotates the token now, ignoring local expiry, to recover from a server-side 401. - session: dialectic_query and _flush_session treat a 401 as a trigger to force one token rotation and retry the call exactly once. A persistent auth failure raises HonchoAuthError (dialectic) or records the failure (sync) instead of being returned as an empty result. - provider: injects a one-time notice into the memory context so the model tells the user memory is paused and 'hermes honcho setup' restores it. Auth failures no longer widen the dialectic cadence backoff. New tests cover the exchange retry, invalid_grant terminality plus re-login recovery, forced refresh, 401 retry on both the sync and dialectic paths, the one-time notice, and the backoff exemption. |
||
|
|
2aa359ea70 |
feat(honcho): add OAuth device-code login (RFC 8628) for headless environments
Adds a device authorization grant flow alongside the existing loopback OAuth flow, so `hermes setup` can connect to Honcho cloud from SSH and other no-browser environments. - oauth.py: new HTTP seams — _http_post_form_status (non-raising, since RFC 8628 polling reads the OAuth error off a 400) and _http_get_json for the RFC 8414 metadata probe - oauth_flow.py: DeviceCode, request_device_code, poll_for_token with slow_down backoff (+5s, capped at 60s) bounded by expires_in, typed errors (AccessDenied, DeviceCodeExpired, AuthorizationTimeout), and supports_device_login (fail-closed metadata gate); device flow ends in the same install_grant tail as loopback so refresh/status work unchanged - oauth_flow.py: loopback callback now serves a "sign-in was not completed" page on consent cancel instead of the success page - cli.py: cloud menu offers oauth / device / apikey; the device option only appears when the host advertises the grant, and becomes the default when no browser is detected - 18 new tests covering the full flow against a local fake AS, backoff schedule, error mapping, deadline bound, metadata gate, and wizard branches Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ba9e3a491b |
feat(memory): Honcho OAuth connect — desktop and CLI flows + token refresh (#44335)
* feat(memory): OAuth token storage and refresh for the Honcho provider * feat(memory): refresh the Honcho OAuth token in the client and session * feat(memory): zero-CLI loopback OAuth authorization flow * feat(memory): generic memory-provider OAuth connect endpoints * feat(desktop): memory-provider OAuth connect link * feat(memory): CLI OAuth sign-in with source-tagged authorize links * fix(memory): IP-literal loopback redirect and consent config_path on the authorize link * fix(memory): profile-scope the memory-provider OAuth endpoints * refactor(desktop): generic memory-provider OAuth client functions * docs(memory): trim OAuth module docstrings to the invariants * docs(memory): document OAuth connect as an optional auth method * fix(memory): send home-relative display path to consent, not the absolute path * perf(memory): cache OAuth token expiry in memory to skip the hot-path disk read * fix(memory): log OAuth refresh failures at warning, not debug * feat(memory): fall back to an OS-assigned loopback port when 8765 is taken * test(memory): cover the desktop Connect launcher, status, and provider dispatch * fix(desktop): keep the memory-provider dropdown one size regardless of connect state * fix(desktop): move the memory connect link to the description line, leaving the dropdown untouched * refactor(memory): move OAuth connect routes out of web_server into a memory-layer router * refactor(desktop): import MemoryConnect directly, drop the single-export barrel * fix(memory): launch CLI OAuth sign-in right after the auth choice, not after the wizard * fix(desktop): auto-clear the OAuth error state instead of leaving it sticky * test(honcho): isolate auth-method prompt from deployment-shape wizard tests main's wizard suite scripts the cloud prompts without the OAuth auth-method step; auto-answer it in the shared helper so the answer lists stay shape-only. * docs(honcho): document query-adaptive reasoning level (reasoningHeuristic) README never mentioned reasoningHeuristic and listed reasoningLevelCap as an orphaned cap with the wrong default (— vs "high"). Add the query-adaptive scaling note + the reasoningHeuristic/reasoningLevelCap rows (grouped under Dialectic & Reasoning), matching the wording already on the hosted honcho.md page, and add a pointer from the memory-providers overview. * fix(honcho): default the CLI peer prompt to the OAuth consent name The CLI runs the grant with apply_config=False, so the peerName the user just entered at consent was dropped and the wizard's 'Your name' prompt fell back to $USER. Surface it as a transient OAuthCredential.consent_peer_name (set even when config isn't merged) and seed the prompt default from it. * feat(honcho): split OAuth client_id by surface (cli=hermes-agent, desktop=hermes-desktop) resolve_endpoints now picks the client_id from the initiating surface and threads it through authorize -> token exchange -> persisted grant -> refresh, so the CLI and desktop register as distinct OAuth clients. Surface-specific env overrides (HONCHO_OAUTH_CLIENT_ID_CLI/_DESKTOP) win over the generic HONCHO_OAUTH_CLIENT_ID, which still overrides every surface. * feat(honcho): show OAuth vs API key in status; detect existing OAuth in setup status now prints 'Auth: OAuth (clientId, token valid Xm/expired)' instead of masking the OAuth access token as a generic API key; setup notes an existing OAuth grant when re-run. * docs(honcho): drop 'shared pool' wording from unified observation mode help * fix(honcho): cross-process lock around OAuth refresh to prevent grant revocation The in-process threading lock can't stop a sibling process (another profile or the desktop app sharing honcho.json) from replaying the single-use refresh token and tripping reuse-detection, which revokes the whole grant. Guard the read-refresh-persist section with an OS file lock on <config>.lock so only one process rotates at a time; the others re-read the freshly-persisted token. Best-effort: platforms without flock degrade to in-process serialization. * refactor(honcho): one OAuth client (hermes-agent) for all surfaces Collapse the per-surface client_id split. CLI and desktop now use a single client_id (hermes-agent); consent branding/UI still adapt via the source query param. One grant identity means no clientId-vs-refresh-token desync that could get the grant revoked. HONCHO_OAUTH_CLIENT_ID still overrides for self-hosting. * fix(honcho): per-session resolves to session_id, never remapped by title Reorder resolve_session_name so stable identifiers win over labels: gateway per-chat key first, then the per-session session_id, then the cwd map / title. A (possibly auto-generated) title can no longer remap a live per-session conversation onto a second Honcho session mid-stream — fixes the desktop, which is per-conversation via session_id. Consequence: a gateway's per-chat key now also wins over a title (titles never remap a stable id). |