5 Commits
Author SHA1 Message Date
Teknium 5dc648c2af fix(plugins): close the review gaps in plugin OAuth cleanup and error scrubbing
Greptile on #141510: platform secrets (MATRIX_PASSWORD, DINGTALK_CLIENT_SECRET) passed the
client-variable check, so it now uses is_registered_env_name like the rest of config. Uninstall
deleted grants by manifest key while sign-in stores them by directory key; both use plugin_key
now. A core resolver's own refresh POST (Codex) could raise httpx with the refresh-token request
attached; that is rebuilt too. Logout no longer needs a valid oauth: entry, a numeric id is
refused instead of accepted and unusable, and the host-domain warning keeps three labels for
two-letter country suffixes (vendor.co.uk).
2026-10-19 07:09:56 -07:00
Teknium 5a47d14aa2 fix(plugins): errors from a signed-in plugin request carry no credential
credentialed_provider_request raised httpx and AuthError exceptions straight to plugin code. A
ConnectError keeps the request it tried to send (exc.request.headers holds the bearer), and an
AuthError from a failed refresh chained to the httpx error whose request body held the refresh
token and the client secret; the traceback frames held them too. Errors past the origin check are
now rebuilt without request, chain or traceback, and the token POST raises outside its handler.
The Codex lane (#136117) shares this path and gains the same fix.
2026-10-19 07:09:56 -07:00
Ilya Balobanov 0d8005121e fix(plugins): tighten plugin OAuth clients after review
- the resolver that returns the bearer is core-only (_plugin_oauth_spec),
  like the _PROVIDERS resolvers; plugins go through
  credentialed_provider_request
- a token endpoint answer counts only when it is 2xx: a redirect is a
  failure, never a token
- `oauth:` given as a mapping fails instead of reading as undeclared
- an authorize or token URL with a fragment, a bad IPv6 literal or a bad
  port is a validate problem instead of a crash
- plugin code is read as utf-8-sig when looking for the token URL
- tests: a refresh answered without a new refresh token keeps the old
  one (Google does this), every field of the entry changes the
  fingerprint, and the install notice command resolves a plugin whose
  manifest name differs from its directory; the FakeIdP knob is now
  token_answer, since it also answers 200

Refs #139077
2026-10-19 07:09:56 -07:00
Ilya Balobanov b1b4effe51 feat(plugins): OAuth clients a plugin declares, signed in and refreshed by Hermes
A plugin that calls a third-party API with the user's own OAuth client
(Google, Microsoft, Notion) declares it under `oauth:` in plugin.yaml
instead of running the consent flow and keeping the refresh token itself.

- `hermes auth add|status|refresh|logout <plugin>:<id>` runs the PKCE
  login of auth_oauth_pkce_plugin and keeps the grant in the active
  profile's auth.json (section `plugin_oauth`, keyed by the plugin's key)
- `credentialed_provider_request(<id>, ...)` attaches the bearer for the
  entry's declared hosts only (https, port 443) and refreshes the grant
  under the auth-store lock; the calling plugin's own entry is resolved
  before `_PROVIDERS`, so a later core provider of the same name cannot
  take it over
- each grant stores a fingerprint of the entry it was made for: a plugin
  version that changes the token URL, hosts, scopes or client variables
  gets ProviderNotSignedIn until the user signs in again
- the client secret is read with get_secret right before each token POST
  (OAuthPKCEConfig.token_request) and never sits in the frozen config
- grant-dead codes retire the grant; invalid_client, unauthorized_client
  and invalid_scope surface as AuthError codes; a sign-in granted fewer
  scopes than declared saves nothing
- `hermes plugins validate` checks the block and the declared variables
  and warns when plugin code names the token URL; install/show notices
  and the catalog re-pin consent delta list the entries

Refs #139077
2026-10-19 07:09:56 -07:00
Teknium 4070f011a2 feat(plugins): core-made Codex requests so plugins never hold the token
Plugins that need the user's ChatGPT/Codex subscription (hermes-live-voice mints
an OpenAI Realtime client secret) vendored their own reader + refresher for
~/.codex/auth.json. Codex refresh tokens are single-use, so that second refresher
races Hermes's locked refresh and can revoke the user's whole token family.

hermes_cli.plugin_provider_requests.credentialed_provider_request() lets core make
the request instead: it resolves the credential through
resolve_codex_runtime_credentials (Hermes's locked refresh, profile-scoped),
attaches Authorization itself, sends only to chatgpt.com / api.openai.com, never
follows redirects, and returns status/headers/body. Callers are attributed by the
plugin directory on the call stack and must declare `requires_auth: [openai-codex]`
in plugin.yaml; the declaration shows at install, in `hermes plugins show`, and a
catalog re-pin that adds it asks for consent. A plain importable function so
dashboard plugin_api.py modules can use it.
2026-10-10 10:43:32 -07:00