feat(auth): add Google Vertex Express x-goog-api-key header support - #4375
feat(auth): add Google Vertex Express x-goog-api-key header support#4375ysnock404 wants to merge 33 commits into
Conversation
Google Vertex Express API requires x-goog-api-key header instead of Authorization: Bearer. When the custom provider base_url contains aiplatform.googleapis.com, pass the api_key via x-goog-api-key header and use a placeholder api_key to satisfy the OpenAI SDK validation. This enables using google-vertex-express as a custom provider in config.yaml with the Vertex Express API key directly.
* fix: root-level provider in config.yaml no longer overrides model.provider
load_cli_config() had a priority inversion: a stale root-level
'provider' key in config.yaml would OVERRIDE the canonical
'model.provider' set by 'hermes model'. The gateway reads
model.provider directly from YAML and worked correctly, but
'hermes chat -q' and the interactive CLI went through the merge
logic and picked up the stale root-level key.
Fix: root-level provider/base_url are now only used as a fallback
when model.provider/model.base_url is not set (never as an override).
Also added _normalize_root_model_keys() to config.py load_config()
and save_config() — migrates root-level provider/base_url into the
model section and removes the root-level keys permanently.
Reported by (≧▽≦) in Discord: opencode-go provider persisted as a
root-level key and overrode the correct model.provider=openrouter,
causing 401 errors.
* fix(security): redact secrets from execute_code sandbox output
The execute_code sandbox stripped env vars with secret-like names from
the child process (preventing os.environ access), but scripts could
still read secrets from disk (e.g. open('~/.hermes/.env')) and print
them to stdout. The raw values entered the model context unredacted.
terminal_tool and file_tools already applied redact_sensitive_text()
to their output — execute_code was the only tool that skipped this
step. Now the same redaction runs on both stdout and stderr after
ANSI stripping.
Reported via Discord (not filed on GitHub to avoid public disclosure
of the reproduction steps).
Telegram API returns HTTP 400 when sent whitespace-only or empty text. Add a guard at the top of send() to silently succeed on blank content instead of crashing. Equivalent to OpenClaw NousResearch#56620.
…ch#4390) After a successful write_file or patch, update the stored read timestamp to match the file's new modification time. Without this, consecutive edits by the same task (read → write → write) would false-warn on the second write because the stored timestamp still reflected the original read, not the first write. Also renames the internal tracker key from 'file_mtimes' to 'read_timestamps' for clarity.
…allback on 429 (NousResearch#4361) Three bugs prevented credential pool rotation from working when multiple Codex OAuth tokens were configured: 1. credential_pool was dropped during smart model turn routing. resolve_turn_route() constructed runtime dicts without it, so the AIAgent was created without pool access. Fixed in smart_model_routing.py (no-route and fallback paths), cli.py, and gateway/run.py. 2. Eager fallback fired before pool rotation on 429. The rate-limit handler at line ~7180 switched to a fallback provider immediately, before _recover_with_credential_pool got a chance to rotate to the next credential. Now deferred when the pool still has credentials. 3. (Non-issue) Retry budget was reported as too small, but successful pool rotations already skip retry_count increment — no change needed. Reported by community member Schinsly who identified all three root causes and verified the fix locally with multiple Codex accounts.
_show_status() now references self.provider and self._provider_source, added after the original PR was submitted.
backdrop-filter on .navbar creates a new CSS stacking context that hides .navbar-sidebar menu content on mobile (only the close button is visible). Scope the blur effect to min-width: 997px so it only applies on desktop where the sidebar is not rendered inside the navbar. Ref: facebook/docusaurus#6996, facebook/docusaurus#6853
Target the exact state that breaks: when .navbar-sidebar--show is active on the same <nav> element. This preserves the blur on mobile when the sidebar is closed, and only removes it when the sidebar is open.
When a dangerous command was blocked and the user approved it via /approve, the command was executed but the agent loop had already exited — the agent never received the command output and the task died silently. Now _handle_approve_command sends immediate feedback to the user, then creates a synthetic continuation message with the command output and feeds it through _handle_message so the agent picks up where it left off. - Send command result to chat immediately via adapter.send() - Create synthetic MessageEvent with command + output as context - Spawn asyncio task to re-invoke agent via _handle_message - Return None (feedback already sent directly) - Add test for agent re-invocation after approval - Update existing approval tests for new return behavior
The openai SDK's SyncAPIClient.is_closed is a method, not a property. getattr(client, 'is_closed', False) returned the bound method object, which is always truthy — causing _is_openai_client_closed() to report all clients as closed and triggering unnecessary client recreation (~100-200ms TCP+TLS overhead per API call). Fix: check if is_closed is callable and call it, otherwise treat as bool. Fixes NousResearch#4377 Co-authored-by: Bartok9 <Bartok9@users.noreply.github.com>
…ch#4412) Replace the per-response padding from PR NousResearch#4359 (which created a void between short responses and the prompt) with a one-time initial scroll at session start. Prints terminal_height newlines before the banner so the cursor starts at the bottom row — banner, responses, and prompt all appear pinned to the bottom with empty space above, not below. patch_stdout naturally keeps the prompt at the bottom from there, so no per-response padding is needed.
- Add _DEFAULT_EXPORT_EXCLUDE_ROOT constant with 25+ entries to exclude from default profile exports: repo checkout (hermes-agent), worktrees, databases (state.db), caches, runtime state, logs, binaries - Add _default_export_ignore() with root-level and universal exclusions (__pycache__, *.sock, *.tmp at any depth) - Remove redundant shutil/tempfile imports from contributor's if-block - Block import_profile() from accepting 'default' as target name with clear guidance to use --name - Add 7 tests covering: archive creation, inclusion of profile data, exclusion of infrastructure, nested __pycache__ exclusion, import rejection without --name, import rejection with --name default, full export-import roundtrip with a different name Addresses review feedback on PR NousResearch#4370.
Show inline diffs in the CLI transcript when write_file, patch, or skill_manage modifies files. Captures a filesystem snapshot before the tool runs, computes a unified diff after, and renders it with ANSI coloring in the activity feed. Adds tool_start_callback and tool_complete_callback hooks to AIAgent for pre/post tool execution notifications. Also fixes _extract_parallel_scope_path to normalize relative paths to absolute, preventing the parallel overlap detection from missing conflicts when the same file is referenced with different path styles. Gated by display.inline_diffs config option (default: true). Based on PR NousResearch#3774 by @kshitijk4poor.
…search#4428) The total_tokens field includes cache_read + cache_write tokens, but the display only showed input + output — making the math look wrong (e.g. 765K + 134K displayed but total said 9.2M). Now shows a cache line when cache tokens are present so all visible numbers sum to the displayed total. Affects both terminal (hermes insights) and gateway (/insights) formats.
…s category (NousResearch#4435) The unified skill from PR NousResearch#4332 was placed at a top-level skills/hermes-agent/ directory, creating a redundant standalone category. Move it to skills/autonomous-ai-agents/hermes-agent/ alongside claude-code, codex, and opencode where it belongs.
…(salvage NousResearch#4400) (NousResearch#4419) Adds two Camofox features: 1. Persistent browser sessions: new `browser.camofox.managed_persistence` config option. When enabled, Hermes sends a deterministic profile-scoped userId to Camofox so the server maps it to a persistent browser profile directory. Cookies, logins, and browser state survive across restarts. Default remains ephemeral (random userId per session). 2. VNC URL discovery: Camofox /health endpoint returns vncPort when running in headed mode. Hermes constructs the VNC URL and includes it in navigate responses so the agent can share it with users. Also fixes camofox_vision bug where call_llm response object was passed directly to json.dumps instead of extracting .choices[0].message.content. Changes from original PR: - Removed browser_evaluate tool (separate feature, needs own PR) - Removed snapshot truncation limit change (unrelated) - Config.yaml only for managed_persistence (no env var, no version bump) - Rewrote tests to use config mock instead of env var - Reverted package-lock.json churn Co-authored-by: analista <psikonetik@gmail.com.com>
…NousResearch#4414) * feat(skills): add content size limits for agent-created skills Agent writes via skill_manage (create/edit/patch/write_file) are now constrained to prevent unbounded growth: - SKILL.md and supporting files: 100,000 character limit - Supporting files: additional 1 MiB byte limit - Patches on oversized hand-placed skills that reduce the size are allowed (shrink path), but patches that grow beyond the limit are rejected Hand-placed skills and hub-installed skills have NO hard limit — they load and function normally regardless of size. Hub installs get a warning in the log if SKILL.md exceeds 100k chars. This mirrors the memory system's char_limit pattern. Without this, the agent auto-grows skills indefinitely through iterative patches (hermes-agent-dev reached 197k chars / 72k tokens — 40x larger than the largest skill in the entire skills.sh ecosystem). Constants: MAX_SKILL_CONTENT_CHARS (100k), MAX_SKILL_FILE_BYTES (1MiB) Tests: 14 new tests covering all write paths and edge cases * feat(skills): add fuzzy matching to skill patch _patch_skill now uses the same 8-strategy fuzzy matching engine (tools/fuzzy_match.py) as the file patch tool. Handles whitespace normalization, indentation differences, escape sequences, and block-anchor matching. Eliminates exact-match failures when agents patch skills with minor formatting mismatches.
…ousResearch#4419) (NousResearch#4440) PR NousResearch#4419 was based on pre-credential-pools main where _config_version was 10. The squash merge downgraded it from 11 (set by NousResearch#2647) back to 10. Also fixes the test assertion.
When `sudo hermes gateway install --system --run-as-user <user>` generates the systemd unit, get_hermes_home() resolves to /root/.hermes because Path.home() returns root's home under sudo. The unit correctly sets HOME= and User= via _system_service_identity(), but HERMES_HOME was computed independently and pointed to root's config directory. Add _hermes_home_for_target_user() which remaps the current HERMES_HOME to the equivalent path under the target user's home. This handles: - Default ~/.hermes → target user's ~/.hermes - Profiles (e.g. ~/.hermes/profiles/coder) → preserves relative structure - Custom paths (e.g. /opt/hermes) → kept as-is Supersedes NousResearch#3861 which only handled the default case and left profiles broken (also flagged by Copilot review). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The library removed the static get_transcript() method in v1.0. Migrate to the new instance-based fetch() API and normalize FetchedTranscriptSnippet objects back to dicts for compatibility with the rest of the script.
By default 'hermes gateway run' now prints WARNING+ to stderr so connection errors and startup failures are visible in the terminal without having to tail ~/.hermes/logs/gateway.log. - gateway/run.py: start_gateway() accepts verbosity: Optional[int]=0. When not None, attaches a StreamHandler to stderr with level mapped from the count (0=WARNING, 1=INFO, 2+=DEBUG). Root logger level is also lowered when DEBUG is requested so records are not swallowed. - hermes_cli/gateway.py: run_gateway() gains verbose: int and quiet: bool params. -q translates to verbosity=None (no stderr handler). Wired through gateway_command(). - hermes_cli/main.py: -v changed from store_true to action=count so -v/-vv/-vvv each increment the level. -q/--quiet added as a new flag. Behaviour summary: hermes gateway run -> WARNING+ on stderr (default) hermes gateway run -q -> silent hermes gateway run -v -> INFO+ hermes gateway run -vv -> DEBUG
…mock - stderr handler now uses RedactingFormatter to match file handlers - restart path uses verbose=0 (int) instead of verbose=False (bool) - test mock updated with new run_gateway(verbose, quiet, replace) signature
The original PR excluded auth.json from _DEFAULT_EXPORT_EXCLUDE_ROOT and filtered both auth.json and .env from named profile exports, but missed adding .env to the default profile exclusion set. Default exports would still leak .env containing API keys. Added .env to _DEFAULT_EXPORT_EXCLUDE_ROOT, added test coverage, and updated the existing test that incorrectly asserted .env presence.
…conditions
When PyYAML is unavailable or YAML frontmatter is malformed, the fallback
parser may return metadata as a string instead of a dict. This causes
AttributeError when calling .get("hermes") on the string.
Added explicit type checks to handle cases where metadata or hermes fields
are not dicts, preventing the crash.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…inuity Allow callers to pass X-Hermes-Session-Id in request headers to continue an existing conversation. When provided, history is loaded from SessionDB instead of the request body, and the session_id is echoed in the response header. Without the header, existing behavior is preserved (new uuid per request). This enables web UI clients to maintain thread continuity without modifying any session state themselves — the same mechanism the gateway uses for IM platforms (Telegram, Discord, etc.).
Reuse a single SessionDB across requests by caching on self._session_db with lazy initialization. Avoids creating a new SQLite connection per request when X-Hermes-Session-Id is used. Updated tests to set adapter._session_db directly instead of patching the constructor.
Google Vertex Express API requires x-goog-api-key header instead of Authorization: Bearer. When the custom provider base_url contains aiplatform.googleapis.com, pass the api_key via x-goog-api-key header and use a placeholder api_key to satisfy the OpenAI SDK validation. This enables using google-vertex-express as a custom provider in config.yaml with the Vertex Express API key directly.
…nto feat/google-vertex-express-auth
|
Note: This PR touches 40+ files across unrelated areas (browser, skills, tests, gateway platforms). The actual feature change appears limited to run_agent.py for the x-goog-api-key header injection. The PR likely needs rebasing or splitting — the diff includes many unrelated changes. |
|
Note: This PR touches 40+ files across unrelated areas. Needs rebasing or splitting. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the Vertex Express contribution. The requested API-key path is still not present on current main, but this branch needs a focused salvage rather than a direct application.
Problems
- The changed initialization hunk is stale: explicit OpenAI client construction now lives in
agent/agent_init.py:892-948; it currently retains the realapi_keyatagent/agent_init.py:904-910. - A port also needs to cover rebuilds: credential rotation resets the key and rebuilds the client in
run_agent.py:4527-4532. - The PR's
tests/test_run_agent.pyadditions do not exercise Vertex Express header/key construction. - The branch contains 43 changed files across unrelated subsystems, as noted by the existing maintainer comments.
Suggested changes
- Salvage the header behavior into the current initializer and header-reapplication path, with construction and rebuild regression tests.
- Keep the salvage limited to this auth behavior; current
custom_providers.extra_headersplumbing (hermes_cli/config.py:5069-5089) adds headers but does not replace the SDK API key.
Automated hermes-sweeper review.
| @@ -829,6 +837,14 @@ def __init__( | |||
| client_kwargs["default_headers"] = { | |||
There was a problem hiding this comment.
This initialization branch has moved to agent/agent_init.py:892-948 on current main. A salvage needs to port this behavior there and update the client-header reapplication path used after credential rotation (run_agent.py:4527-4532), otherwise a rebuilt client returns to the real API key/Bearer path.
Summary
Google Vertex Express API requires
x-goog-api-keyheader instead ofAuthorization: Bearer. This PR adds detection foraiplatform.googleapis.comURLs and automatically switches to the correct header.Changes
run_agent.py: Wheneffective_basecontainsaiplatform.googleapis.com, pass the API key viax-goog-api-keyheader and use a placeholderapi_keyto satisfy the OpenAI SDK validationHow it works
Users configure a custom provider in
config.yaml:The agent then correctly authenticates with Vertex Express using
x-goog-api-keyheader.