Skip to content
This repository was archived by the owner on Jul 4, 2026. It is now read-only.

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
kingsleydon pushed a commit that referenced this pull request May 27, 2026
…y prefixed

Companion to the NousResearchGH-25255 incoming-strip fix from @hayka-pacha. Without
this, build_anthropic_kwargs unconditionally added 'mcp_' to every tool
name in step 3, so a native MCP server tool registered as
'mcp_composio_X' was sent as 'mcp_mcp_composio_X' on the wire. The
incoming strip only removes ONE prefix, which still worked on first
call, but on subsequent calls the model pattern-matched the
single-prefixed form from message history and produced names that
stripped to 'composio_X' — registry miss, dispatch fail.

The history-rewrite block (#4) already has this guard. Apply the same
guard to the schema-rewrite block (#3) so round-trip is symmetric.

Added 4 outgoing-side tests. Existing 7 incoming-side tests still pass.

Author map: hayka-pacha added for PR NousResearch#25270 salvage attribution.

Refs NousResearchGH-25255.
kingsleydon pushed a commit that referenced this pull request Jun 3, 2026
…NousResearch#34192) (NousResearch#34382)

NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup
with:

  /usr/bin/tini: No such file or directory

The image moved from tini to s6-overlay as PID 1 (/init) earlier in
2026. Orchestration templates that still pin /usr/bin/tini as the
entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no
binary to exec and the container crashes immediately.

Hermes has no control over the Hostinger catalog template, but we can
make the image backward-compatible by symlinking /usr/bin/tini -> /init
during the s6-overlay install step. External wrappers that exec
/usr/bin/tini will land on the same s6-overlay reaper they would have
landed on if they'd used the canonical /init entrypoint.

The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim
is purely for legacy external wrappers, not for the image's own
runtime path. Once affected catalogs are updated, the symlink can be
removed.

Other issues NousResearch#34192 raises that are NOT addressed by this PR:

  * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148
    (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger
    template likely needs to update its env-var propagation.

  * Problem #3 (incompatible session formats): RFC for pluggable
    SessionDB is tracked in NousResearch#23717.

  * Problem #4 (Telegram polling conflict): an operations problem on
    Hostinger's side, not in this codebase.

This PR is scoped to the one issue that can be fixed inside
Dockerfile: the missing /usr/bin/tini binary.

Tests (3 in test_dockerfile_tini_compat_shim.py):

  - test_tini_compat_symlink_present
    Guard: the symlink line must exist in Dockerfile.
  - test_tini_compat_comment_explains_why
    The NousResearch#34192 anchor comment must be present so future readers know
    why the shim is there (avoid accidental removal).
  - test_entrypoint_still_init_not_tini
    Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is
    only for external wrappers.

Refs: NousResearch#34192
Partial fix: addresses the immediate tini-binary crash. Catalog-side
fixes still needed by Hostinger for the UID and session-format
problems documented in the issue.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant