Skip to content

feat: zero-config aux + plain non-secret env values for reverse-proxied use#4

Closed
kingsleydon wants to merge 2 commits into
mainfrom
feat/aux-credentials-passthrough
Closed

feat: zero-config aux + plain non-secret env values for reverse-proxied use#4
kingsleydon wants to merge 2 commits into
mainfrom
feat/aux-credentials-passthrough

Conversation

@kingsleydon
Copy link
Copy Markdown

Summary

Two narrow additive changes that make Hermes usable end-to-end on a
reverse-proxied deployment (one with a static api_key in config but no
local OAuth state). Both are gated so single-user CLI workflows are
byte-identical to before.

Why

Clawdi runs Hermes inside per-user containers behind a controller that
talks to a sub2api-compatible Codex Responses endpoint via the
model.api_key Direct config path PR #2 added. Two gaps surfaced:

1. Auxiliary tasks all break for direct-config users

_try_codex() only ever reads from the Codex auth store / credential
pool. When the operator pinned an explicit api_key in config.yaml
(the PR #2 path), every auxiliary task — vision, web_extract,
compression, session_search, skills_hub, approval, mcp,
flush_memories — falls into the OAuth chain and returns
None, None, surfacing as the loud startup banner

⚠ No auxiliary LLM provider configured — context compression will
drop middle turns without a summary.

…even though the operator's main-inference api_key would work fine
for these tasks.

2. Non-secret env vars are unreadable

GET /api/env always redacts redacted_value, even for fields
marked password: False in OPTIONAL_ENV_VARS (allow-lists, mode
flags, account IDs). Reverse-proxied dashboards can't round-trip
those fields in form inputs without forcing the user to retype on
every edit.

What this PR changes

agent/auxiliary_client.py: codex aux direct-config path

_try_codex() learned a new entry point —
_try_codex_from_config() — that mirrors the Direct config path
runtime_provider.py added for main inference. When
model.provider == \"openai-codex\" AND model.api_key is set,
build a Codex auxiliary client straight from those values instead of
going through the device-code OAuth flow.

The existing pool / _read_codex_access_token chain is untouched
and runs unchanged whenever api_key is empty, so single-user CLI
workflows see no behavior change.

The auxiliary model defaults to model.default (the operator's
main model) rather than _CODEX_AUX_MODEL. In direct-config mode
the only models guaranteed to exist are the ones the operator's
reverse proxy actually serves — which is what model.default
describes. _CODEX_AUX_MODEL is hardcoded for ChatGPT-backed
Codex compatibility and may not exist on a custom endpoint.
Per-task auxiliary.{task}.model overrides still win because the
caller composes them via model or default.

Net result: operators only have to set
model.default + model.base_url + model.api_key once. All eight
auxiliary tasks pick the same model automatically — same zero-config
UX an OAuth user gets, just sourced from explicit config instead of
OAuth tokens.

hermes_cli/web_server.py: plain value for non-secret env vars

GET /api/env now returns a value field alongside
redacted_value. For env vars marked password: False, value
is the plain on-disk string; for password: True fields it stays
None.

Backward compatible — redacted_value is unchanged so existing
clients keep working. Password-flagged fields still require the
rate-limited /api/env/reveal endpoint with audit logging.

Compatibility matrix

User class Behavior change
Single-user OAuth (model.api_key == \"\") None — _try_codex_from_config short-circuits on empty api_key, falls through to the unchanged OAuth path
Explicit api_key (PR #2 path) Auxiliary tasks now work instead of warning at startup
Reverse-proxied / sub2api deployments Both gaps closed
Old API clients reading redacted_value None — new value field is purely additive

Test plan

  • hermes web with no env override → _try_codex reaches OAuth path, _CODEX_AUX_MODEL returned (single-user CLI baseline)
  • config.yaml with model.provider=openai-codex, model.api_key=sk-xxx, model.base_url=https://..._try_codex_from_config returns (CodexAuxiliaryClient, model.default)
  • config.yaml with model.provider=openai-codex, model.api_key=\"\"_try_codex_from_config returns (None, None), OAuth path runs unchanged
  • Per-task override auxiliary.compression.model=gpt-5.1-codex-mini → caller's final_model wins over model.default
  • GET /api/env for TELEGRAM_BOT_TOKEN (password: True) → value is None
  • GET /api/env for TELEGRAM_ALLOWED_USERS (password: False) → value is the plain on-disk string

…ed use

Two narrow additive changes that only affect deployments that have
already opted in via PR #2's Direct config path. Single-user CLI flows
and OAuth users are byte-identical to before.

## auxiliary_client.py: codex aux direct config

`_try_codex()` learned a new entry point — `_try_codex_from_config()`
— that mirrors the Direct config path runtime_provider.py added in
PR #2 for the main inference flow. When `model.provider == "openai-codex"`
AND `model.api_key` is set, build a Codex auxiliary client straight
from those values instead of going through the device-code OAuth
flow.

This unblocks every auxiliary task (vision, web_extract, compression,
session_search, skills_hub, approval, mcp, flush_memories) for
reverse-proxied deployments where there's no local OAuth state. The
existing pool / `_read_codex_access_token` chain is untouched and runs
unchanged whenever `api_key` is empty, so single-user CLI workflows
see no behavior change.

The auxiliary model defaults to `model.default` (the operator's main
model) rather than `_CODEX_AUX_MODEL`. In direct-config mode the only
models guaranteed to exist are the ones the operator's reverse proxy
serves, which is what `model.default` describes; `_CODEX_AUX_MODEL`
is hardcoded for ChatGPT-backed Codex compatibility and may not
exist on a custom endpoint. Per-task `auxiliary.{task}.model`
overrides still win because the caller composes them via
`model or default`.

Net result: operators only have to set `model.default + model.base_url
+ model.api_key` once. All eight auxiliary tasks pick the same model
automatically — same zero-config UX an OAuth user gets, just sourced
from explicit config instead of OAuth tokens.

## web_server.py: plain `value` for non-secret env vars

`GET /api/env` now returns a `value` field alongside `redacted_value`.
For env vars marked `password: False` in OPTIONAL_ENV_VARS (allow-lists,
mode flags, account IDs, etc.), `value` is the plain on-disk string;
for `password: True` fields it stays None.

Backward compatible: `redacted_value` is unchanged so existing clients
keep working. Dashboards that want to round-trip non-secret fields
through form inputs can now read `value` instead of forcing the user
to retype on every edit.

Password-flagged fields still require the rate-limited
`/api/env/reveal` endpoint with audit logging — no change there.
…odex branch + log load_config failures

Two follow-ups from review of the previous commit:

1. The codex branch in resolve_provider_client() reads OAuth tokens
   even when callers pass explicit_api_key/explicit_base_url. The
   raw_codex=True path (run_agent.py:923-925, 5601-5614) and any
   fallback_providers entry that targets openai-codex would silently
   fall through to OAuth and fail in reverse-proxied deployments
   without local Codex auth state.

   Added a direct-credentials short-circuit at the top of the branch:
   when explicit_api_key is set, build the OpenAI client straight from
   it (and explicit_base_url if provided, otherwise _CODEX_AUX_BASE_URL).
   raw_codex returns the unwrapped client; the standard path returns
   the CodexAuxiliaryClient-wrapped variant. OAuth flow below remains
   the fallback when explicit_api_key is empty.

2. The except-Exception in _try_codex_from_config() was silent. Added
   a logger.warning so config-load failures surface in pod logs
   instead of being eaten and presenting as "no provider found".
@kingsleydon
Copy link
Copy Markdown
Author

Superseded — switching Clawdi to upstream-native provider: custom + api_mode: codex_responses instead of patching the codex provider path. The auxiliary plumbing fix turns out to be unnecessary because vanilla Hermes already routes custom provider with explicit api_mode through CodexAuxiliaryClient via _wrap_if_needed. Closing this PR; only PR #3 (HERMES_SESSION_TOKEN env override) remains as a fork-level patch and will be proposed upstream separately.

@kingsleydon kingsleydon deleted the feat/aux-credentials-passthrough branch April 15, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant