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

feat: support explicit api_key in config for Codex provider - #2

Merged
kingsleydon merged 1 commit into
mainfrom
feat/clawdi-integration
Apr 15, 2026
Merged

feat: support explicit api_key in config for Codex provider#2
kingsleydon merged 1 commit into
mainfrom
feat/clawdi-integration

Conversation

@kingsleydon

@kingsleydon kingsleydon commented Apr 14, 2026

Copy link
Copy Markdown

Summary

Single-file, pure-additive patch that lets config.yaml provide the Codex provider's api_key directly, instead of requiring OAuth credentials in hermes-auth-store.

Needed for hosted deployments (e.g. Clawdi pods) that pre-configure a sub2api bearer token per deployment — the pod has no interactive OAuth login and no ~/.hermes/auth/codex.json.

Diff

1 file, +16 / -0, hermes_cli/runtime_provider.py:

if provider == "openai-codex":
    # Direct config: explicit api_key in config.yaml (e.g. sub2api proxy).
    # Takes priority over OAuth credentials from hermes-auth-store.
    cfg_api_key = str(model_cfg.get("api_key") or "").strip()
    if cfg_api_key:
        cfg_base_url = (
            str(model_cfg.get("base_url") or "").strip().rstrip("/")
        )
        return {
            "provider": "openai-codex",
            "api_mode": "codex_responses",
            "base_url": cfg_base_url or DEFAULT_CODEX_BASE_URL,
            "api_key": cfg_api_key,
            "source": "config",
            "requested_provider": requested_provider,
        }
    # Fallback: OAuth credentials from hermes-auth-store
    try:
        creds = resolve_codex_runtime_credentials()
        # ... existing OAuth path, unchanged ...

Priority order

  1. config.yaml model.api_key (new) — hosted deployments
  2. hermes-auth-store (upstream) — interactive OAuth (ChatGPT Plus/Pro)
  3. env-var fallback (upstream) — e.g. OpenRouter on AuthError

Zero behavior change when model.api_key is empty — the existing OAuth path runs unchanged.

Why patch instead of using custom_providers

The provider: custom path does accept model.api_key from config, but:

  • It auto-detects api_mode from URL; sub2api's internal hostname doesn't match the api.openai.com heuristic → defaults to chat_completions, which is the wrong shape for Codex Responses API.
  • Loses openai-codex-specific routing (GPT-5 reasoning params, Codex Responses details).
  • Forces Clawdi's config generator to emit a different shape (custom_providers: [...]) than the model.* convention users expect.

The 16-line patch keeps provider: openai-codex first-class while satisfying the hosted-deployment use case.

Rebase hygiene

  • Rebased onto upstream/main (currently 677f1227) — clean, no conflicts
  • Pure additive insert at the top of the openai-codex branch; existing OAuth code path untouched
  • Insertion point is a stable conditional block header — low rebase-conflict risk going forward

Not for upstream

Internal-only PR against Clawdi-AI/hermes-agent:main. Not submitting upstream to NousResearch/hermes-agent.

When config.yaml contains an explicit `api_key` under the openai-codex
model section, use it directly as Authorization: Bearer instead of
falling through to the OAuth hermes-auth-store. This enables hosted
deployments (e.g. Clawdi) to pre-configure a proxy API key without
requiring interactive `hermes auth`.

Priority: config api_key > OAuth auth-store > env-var fallback.

Co-Authored-By: Hang Yin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kingsleydon
kingsleydon force-pushed the feat/clawdi-integration branch from 066aa4e to 967a261 Compare April 15, 2026 02:59
@github-actions

Copy link
Copy Markdown

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

3280:+        b64 = base64.b64encode(raw).decode("ascii")
8311:+        body = base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8", errors="replace")
8315:+                body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="replace")
8320:+                    body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="replace")

⚠️ WARNING: exec() or eval() usage

Dynamic code execution can hide malicious behavior, especially when combined with base64 or network fetches.

Matches (first 20):

2737:+            proc = await asyncio.create_subprocess_exec(
14689:+            parent_check = self._exec(
14693:+                ls_result = self._exec(
14739:+        result = self._exec(cmd_sorted, timeout=60)
14749:+            result = self._exec(cmd_plain, timeout=60)

⚠️ WARNING: Outbound network calls (POST/PUT)

Outbound POST/PUT requests in new code could be data exfiltration. Verify the destination URLs are legitimate.

Matches (first 10):

4504:+                resp = httpx.post(
6437:+            with urllib.request.urlopen(req, timeout=_GATEWAY_HEALTH_TIMEOUT) as resp:
6992:+        with urllib.request.urlopen(req, timeout=15) as r:
7052:+        with urllib.request.urlopen(url, timeout=10) as r:
12751:+        original_urlopen = ws.urllib.request.urlopen
20362:+        resp = urllib.request.urlopen(req, timeout=10)

⚠️ WARNING: Install hook files modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/memory_setup.py
hermes_cli/setup.py

⚠️ WARNING: marshal/pickle/compile usage

These can deserialize or construct executable code objects.

Matches:

20630:+   - Unsafe deserialization (pickle.loads, yaml.load without SafeLoader)

⚠️ WARNING: CI/CD workflow files modified

Changes to workflow files can alter build pipelines, inject steps, or modify permissions. Verify no unauthorized actions or secrets access were added.

Files:

.github/workflows/contributor-check.yml
.github/workflows/deploy-site.yml
.github/workflows/docker-publish.yml
.github/workflows/docs-site-checks.yml
.github/workflows/nix.yml
.github/workflows/skills-index.yml
.github/workflows/supply-chain-audit.yml
.github/workflows/tests.yml

⚠️ WARNING: Dependency manifest files modified

Changes to dependency files can introduce new packages or change version pins. Verify all dependency changes are intentional and from trusted sources.

Files:

package.json
pyproject.toml
scripts/whatsapp-bridge/package.json

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@kingsleydon kingsleydon reopened this Apr 15, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

3280:+        b64 = base64.b64encode(raw).decode("ascii")
8311:+        body = base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8", errors="replace")
8315:+                body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="replace")
8320:+                    body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="replace")

⚠️ WARNING: exec() or eval() usage

Dynamic code execution can hide malicious behavior, especially when combined with base64 or network fetches.

Matches (first 20):

2737:+            proc = await asyncio.create_subprocess_exec(
14689:+            parent_check = self._exec(
14693:+                ls_result = self._exec(
14739:+        result = self._exec(cmd_sorted, timeout=60)
14749:+            result = self._exec(cmd_plain, timeout=60)

⚠️ WARNING: Outbound network calls (POST/PUT)

Outbound POST/PUT requests in new code could be data exfiltration. Verify the destination URLs are legitimate.

Matches (first 10):

4504:+                resp = httpx.post(
6437:+            with urllib.request.urlopen(req, timeout=_GATEWAY_HEALTH_TIMEOUT) as resp:
6992:+        with urllib.request.urlopen(req, timeout=15) as r:
7052:+        with urllib.request.urlopen(url, timeout=10) as r:
12751:+        original_urlopen = ws.urllib.request.urlopen
20362:+        resp = urllib.request.urlopen(req, timeout=10)

⚠️ WARNING: Install hook files modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/memory_setup.py
hermes_cli/setup.py

⚠️ WARNING: marshal/pickle/compile usage

These can deserialize or construct executable code objects.

Matches:

20630:+   - Unsafe deserialization (pickle.loads, yaml.load without SafeLoader)

⚠️ WARNING: CI/CD workflow files modified

Changes to workflow files can alter build pipelines, inject steps, or modify permissions. Verify no unauthorized actions or secrets access were added.

Files:

.github/workflows/contributor-check.yml
.github/workflows/deploy-site.yml
.github/workflows/docker-publish.yml
.github/workflows/docs-site-checks.yml
.github/workflows/nix.yml
.github/workflows/skills-index.yml
.github/workflows/supply-chain-audit.yml
.github/workflows/tests.yml

⚠️ WARNING: Dependency manifest files modified

Changes to dependency files can introduce new packages or change version pins. Verify all dependency changes are intentional and from trusted sources.

Files:

package.json
pyproject.toml
scripts/whatsapp-bridge/package.json

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

@kingsleydon kingsleydon changed the title feat: Codex proxy auth + default_headers fix for Clawdi integration feat: support explicit api_key in config for Codex provider Apr 15, 2026
@kingsleydon
kingsleydon merged this pull request into main Apr 15, 2026
10 of 16 checks passed
kingsleydon added a commit that referenced this pull request Apr 15, 2026
When config.yaml contains an explicit `api_key` under the openai-codex
model section, use it directly as Authorization: Bearer instead of
falling through to the OAuth hermes-auth-store. This enables hosted
deployments (e.g. Clawdi) to pre-configure a proxy API key without
requiring interactive `hermes auth`.

Priority: config api_key > OAuth auth-store > env-var fallback.

Co-Authored-By: Hang Yin

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kingsleydon added a commit that referenced this pull request Apr 15, 2026
…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.
kingsleydon pushed a commit that referenced this pull request Apr 23, 2026
…#13354)

Classic-CLI /steer typed during an active agent run was queued through
self._pending_input alongside ordinary user input.  process_loop, which
drains that queue, is blocked inside self.chat() for the entire run,
so the queued command was not pulled until AFTER _agent_running had
flipped back to False — at which point process_command() took the idle
fallback ("No agent running; queued as next turn") and delivered the
steer as an ordinary next-turn user message.

From Utku's bug report on PR NousResearch#13205: mid-run /steer arrived minutes
later at the end of the turn as a /queue-style message, completely
defeating its purpose.

Fix: add _should_handle_steer_command_inline() gating — when
_agent_running is True and the user typed /steer, dispatch
process_command(text) directly from the prompt_toolkit Enter handler
on the UI thread instead of queueing.  This mirrors the existing
_should_handle_model_command_inline() pattern for /model and is
safe because agent.steer() is thread-safe (uses _pending_steer_lock,
no prompt_toolkit state mutation, instant return).

No changes to the idle-path behavior: /steer typed with no active
agent still takes the normal queue-and-drain route so the fallback
"No agent running; queued as next turn" message is preserved.

Validation:
- 7 new unit tests in tests/cli/test_cli_steer_busy_path.py covering
  the detector, dispatch path, and idle-path control behavior.
- All 21 existing tests in tests/run_agent/test_steer.py still pass.
- Live PTY end-to-end test with real agent + real openrouter model:
    22:36:22 API call #1 (model requested execute_code)
    22:36:26 ENTER FIRED: agent_running=True, text='/steer ...'
    22:36:26 INLINE STEER DISPATCH fired
    22:36:43 agent.log: 'Delivered /steer to agent after tool batch'
    22:36:44 API call #2 included the steer; response contained marker
  Same test on the tip of main without this fix shows the steer
  landing as a new user turn ~20s after the run ended.
kingsleydon pushed a commit that referenced this pull request Apr 23, 2026
The MCP circuit breaker previously had no path back to the closed
state: once _server_error_counts[srv] reached _CIRCUIT_BREAKER_THRESHOLD
the gate short-circuited every subsequent call, so the only reset
path (on successful call) was unreachable. A single transient
3-failure blip (bad network, server restart, expired token) permanently
disabled every tool on that MCP server for the rest of the agent
session.

Introduce a classic closed/open/half-open state machine:

- Track a per-server breaker-open timestamp in _server_breaker_opened_at
  alongside the existing failure count.
- Add _CIRCUIT_BREAKER_COOLDOWN_SEC (60s). Once the count reaches
  threshold, calls short-circuit for the cooldown window.
- After the cooldown elapses, the *next* call falls through as a
  half-open probe that actually hits the session. Success resets the
  breaker via _reset_server_error; failure re-bumps the count via
  _bump_server_error, which re-stamps the open timestamp and re-arms
  the cooldown.

The error message now includes the live failure count and an
"Auto-retry available in ~Ns" hint so the model knows the breaker
will self-heal rather than giving up on the tool for the whole
session.

Covers tests 1 (half-opens after cooldown) and 2 (reopens on probe
failure); test 3 (cleared on reconnect) still fails pending fix #2.
kingsleydon pushed a commit that referenced this pull request May 9, 2026
* ci(nix): auto-fix stale npm hashes on push to main

When a PR merges to main with updated package-lock.json or package.json
in ui-tui/ or web/, the new auto-fix-main job detects stale npmDepsHash
values and pushes a fix commit directly to main.

This eliminates the recurring manual hash-bump PRs (NousResearch#15420, NousResearch#15314,
NousResearch#15272, NousResearch#15244) by reusing the existing fix-lockfiles --apply pipeline.

The fix commit only touches nix/*.nix files, which are outside the push
path filter (package-lock.json / package.json), so it cannot re-trigger
itself.

Closes NousResearch#15314

* fix(ci): use GitHub App token for auto-fix-main push

GITHUB_TOKEN commits are invisible to workflow triggers (GitHub's
infinite-loop prevention). The auto-fix-main job pushes directly to
main, so the fix commit never triggered downstream nix.yml verification.

Mint a short-lived token via the repo's GitHub App (daimon-nous, APP_ID
+ APP_PRIVATE_KEY secrets) so the push is treated as a real event and
nix.yml fires to verify the corrected hashes.

Tested via workflow_dispatch dry-run: app token minted successfully,
checkout with app token succeeded, fix job correctly gated.

Resolves review feedback from Bugbot (r3144569551).

* ci(nix): rename lockfile check job for required status check

Rename 'check' → 'nix-lockfile-check' so the status check name is
unambiguous when added as a required check on main.

* fix(ci): harden auto-fix-main against races, loops, and silent failures

Address adversarial review findings:

1. Race condition (#1): Job-level concurrency with cancel-in-progress
   collapses back-to-back pushes; ref: main checkout always gets latest
   branch state; explicit push target (origin HEAD:main).

2. Loop prevention (#2): File-whitelist check before commit aborts if
   any file outside nix/{tui,web}.nix was modified, preventing
   accidental self-triggering.

3. Silent infra failures (#8): nix-lockfile-check now fails explicitly
   when fix-lockfiles exits without reporting stale status (catches nix
   setup failures, network errors, script bugs that bypass continue-on-error).

4. Commit traceability (NousResearch#11): Auto-fix commits include source SHA and
   workflow run URL in the commit body.

5. Explicit push target (NousResearch#12): git push origin HEAD:main instead of
   bare git push.

---------

Co-authored-by: alt-glitch <alt-glitch@users.noreply.github.com>
kingsleydon pushed a commit that referenced this pull request May 27, 2026
… contract

Three test classes lock in the NousResearch#30963 fix:

1. TestPartialStreamStubFinishReason — drives _interruptible_streaming_api_call
   through the two recovery branches and asserts:
     - text-only partial → finish_reason="length" (the new behaviour),
     - mid-tool-call partial → finish_reason="stop" (unchanged on purpose).

2. TestLengthContinuationPromptBranching — pure-Python check on the branch
   that picks the continuation prompt by response.id. Locks the network
   error wording for partial-stream-stub vs. the output-length wording
   for everything else.

3. TestConversationLoopPartialStreamContinuation — feeds a stub +
   continuation pair into run_conversation, verifies the loop makes a
   second API call (instead of exiting with text_response(stop)),
   confirms the network-error continuation prompt actually reaches the
   model on call #2, and that final_response stitches both halves.

Refs: NousResearch#30963
kingsleydon pushed a commit that referenced this pull request May 29, 2026
… OAuth gates

Two parallel public-path allowlists drifted: _PUBLIC_API_PATHS in
hermes_cli/web_server.py (legacy _SESSION_TOKEN middleware) and
_GATE_PUBLIC_PREFIXES in hermes_cli/dashboard_auth/middleware.py
(OAuth gate). The legacy list included /api/status (documented as a
non-sensitive read-only liveness target); the OAuth gate's list did not.

Effect: every wildcard-subdomain agent surfaced as STARTING/down to the
portal even though the dashboard was serving correctly. Nous account
service (src/server/agents/fly-provider.ts
getInstanceRuntimeStatus) fetches ``/api/status`` without a cookie
as its sole liveness probe; the OAuth gate's 401 looked identical to
'agent dead' on the portal side.

Fix: lift the allowlist into hermes_cli/dashboard_auth/public_paths.py
and have both middlewares import it. _path_is_public now consults
the shared frozenset first, then falls back to the gate's
auth-bootstrap/static prefix list. Future additions to the public list
hit both gates automatically.

Endpoint inventory (verified safe to remain public):

* /api/status            — version, gateway state, active session count,
                           auth-gate shape. Portal liveness probe target.
* /api/config/defaults   — config-defaults feed for the SPA's Config page
* /api/config/schema     — config schema for the SPA's Config page
* /api/model/info        — model catalogue metadata (context windows)
* /api/dashboard/themes  — theme manifests for the skin engine
* /api/dashboard/plugins — plugin manifests for the dashboard

No user data, no session content, no secrets. Same shape an external
monitoring agent would hit on /healthz.

Tests:

* New: test_gated_status_is_public (regression guard with the NAS
  fly-provider.ts liveness-probe rationale spelled out in the docstring)
* New: test_other_public_api_paths_are_public_under_gate (parametrised
  over the rest of PUBLIC_API_PATHS — proves 401 / 302-to-login is
  never the response)
* New: docker integration check #3 in
  test_dashboard_oauth_gate_engaged_by_default — /api/status
  remains 200 under the gate AND reports auth_required=True so the
  portal can distinguish modes
* Updated: test_full_login_round_trip_unlocks_gated_api now probes
  /api/sessions instead of /api/status (status is public, so it
  can no longer distinguish 'logged in' from 'gate accidentally
  disabled')
* Updated: TestApi401Envelope (the no-cookie / invalid-cookie /
  dead-cookie tests) probes /api/sessions for the same reason
* Updated: docker integration check #2 in
  test_dashboard_oauth_gate_engaged_by_default probes
  /api/sessions to prove the gate is intercepting
* Removed: dead _login() helper in
  test_dashboard_auth_status_endpoint.py (no longer needed since
  /api/status is reachable cold)

Companion to docs/handover/hermes-agent-dashboard-s6-insecure-fix.md
(the --insecure flag fix that shipped earlier).
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