Skip to content

feat: credential proxy daemon — zero-knowledge HTTP/HTTPS broker (#4656) - #4695

Closed
dsr-restyn wants to merge 5 commits into
NousResearch:mainfrom
dsr-restyn:feat/cred-proxy
Closed

dsr-restyn wants to merge 5 commits into
NousResearch:mainfrom
dsr-restyn:feat/cred-proxy

Conversation

@dsr-restyn

Copy link
Copy Markdown
Contributor

Summary

  • Adds a credential proxy daemon that intercepts outbound HTTP/HTTPS from tool subprocesses and substitutes hermes-proxy://<name> placeholders with real credential values at the transport layer
  • The agent process structurally cannot read stored values — there is no read API
  • Uses mitmproxy for proxy/TLS/CA and system keyring for credential storage — no custom crypto

Architecture

Agent process          Credential Proxy           Upstream API
    │                      │                          │
    │ Authorization:       │                          │
    │ hermes-proxy://tok1  │                          │
    │─────────────────────>│                          │
    │                      │  Authorization:          │
    │                      │  sk-real-secret-123      │
    │                      │─────────────────────────>│
    │                      │                          │
    │    <response>        │    <response>            │
    │<─────────────────────│<─────────────────────────│

Changes

New files (cred_proxy/)

File Description
store.py Keyring-backed credential store. Public API: set/list/delete. Internal _get() for substitutor only. No read API.
server.py mitmproxy DumpMaster + CredentialProxyAddon — substitutes placeholders in request headers and bodies. ~70 lines.
substitutor.py Regex-based hermes-proxy://<name> detection and replacement.
daemon.py Process lifecycle: start/stop/status, PID+port file management, stale cleanup.
cli.py `hermes cred-proxy start

Modified files

File Change
tools/environments/local.py _make_run_env() injects http_proxy/https_proxy + combined CA bundle when proxy is running
hermes_cli/main.py hermes cred-proxy subcommand group
pyproject.toml cred-proxy extras + console entry point

Design decisions

  • mitmproxy over custom asyncio MITM: delegates TLS termination, CA management, cert issuance, and Content-Length recalculation to a battle-tested library. Keeps server.py to ~70 lines.
  • System keyring over custom AES-256-GCM: uses OS-native secret storage (GNOME Keyring, macOS Keychain, Windows Credential Locker) via the keyring library. No key derivation, no passphrase prompt, no custom crypto.
  • Profile-aware paths: uses get_hermes_home() so proxy state respects HERMES_HOME / active profile.

Tests

22 tests across 4 files:

  • test_store.py — CRUD, sorted listing, overwrite, missing key errors
  • test_substitutor.py — header/body/query string substitution, mixed known/unknown, empty string
  • test_integration.py — mitmproxy addon end-to-end, no-public-get invariant
  • test_fixes.py — stale PID file cleanup

All tests use an in-memory keyring backend (no system keyring access needed).

Related

Refs #4656, addresses #4429, complements #4432

🤖 Generated with Claude Code

Dakota Secula-Rosell and others added 5 commits April 3, 2026 20:47
Adds a credential proxy subsystem that intercepts outbound HTTP/HTTPS
from tool subprocesses and substitutes hermes-proxy://<name> placeholders
with real credential values. The agent process structurally cannot read
stored values — there is no read API.

New files (cred_proxy/):
- store.py: AES-256-GCM encrypted store, machine-derived key, chmod 600
- ca.py: local CA for HTTPS MITM, per-hostname cert issuance
- substitutor.py: hermes-proxy:// placeholder detection + substitution
- server.py: asyncio HTTP/HTTPS MITM proxy, Unix socket + TCP port
- daemon.py: process lifecycle, PID/port files, stale PID cleanup
- cli.py: hermes-cred-proxy start/stop/status/add/list

Minimal additions to existing files:
- tools/environments/local.py: inject http_proxy/https_proxy/CA bundle
  into subprocess env when proxy is running (additive only)
- hermes_cli/main.py: cred-proxy subcommand group (additive only)
- pyproject.toml: cred-proxy extras + entry point

23 tests passing.

Closes NousResearch#4656
…yring

- server.py: replace 459-line asyncio MITM with mitmproxy DumpMaster + CredentialProxyAddon
- store.py: replace AES-256-GCM with keyring (service: hermes-cred-proxy)
- ca.py: deleted — mitmproxy manages its own CA
- daemon.py: remove Unix socket, start mitmproxy via asyncio.run(run_proxy())
- local.py: update CA cert path to mitmproxy-ca-cert.pem
- pyproject.toml: swap cryptography+aiohttp for mitmproxy+keyring
- tests: update for keyring in-memory backend, addon unit tests

22 tests passing.
daemon.py and server.py both used Path.home() / '.hermes' directly,
breaking multi-profile setups where HERMES_HOME points to a profile
subdirectory. Replace with get_hermes_home() from hermes_constants so
the proxy state dir and CA dir respect the active profile.
Adds a config option for proxy-brokered credential placeholders (NousResearch#4429).
Sandboxed runtimes like NVIDIA OpenShell use credential brokering where
env vars contain placeholders and a network proxy rewrites HTTP headers
with real tokens on egress. Hermes strips these as if they were real
secrets, breaking tools that depend on the placeholder being present.

Config:
  terminal:
    proxy_credentials:
      - SLACK_BOT_TOKEN
      - SLACK_APP_TOKEN

Listed vars bypass the provider-secret blocklist via the existing
env_passthrough mechanism. Output redaction still applies. Registration
is config-driven (not agent-controllable) and runs once, guarded by a
module-level flag set after the try block completes.

Changes:
  hermes_cli/config.py        | +5  proxy_credentials config key
  tools/environments/local.py | +26 one-time registration in both
                                    _sanitize_subprocess_env and _make_run_env

31 additions, 0 deletions. No existing code modified.

Addresses NousResearch#4429
Part of NousResearch#410 (Secure Secrets Management umbrella)
…env blocklist

Vars whose value starts with 'hermes-proxy://' are credential proxy
placeholders, not real secrets. They must reach the cred-proxy daemon
in subprocess env so the proxy can substitute the real value at the
HTTP transport layer.

Without this, a var like:
  CLOUDFLARE_API_TOKEN=hermes-proxy://cf_token

would be stripped by _sanitize_subprocess_env even though it carries
no real credential value.

Real secret values on blocked keys are still stripped as before.

Tests: 2 new cases in test_env_passthrough.py
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