mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
The Desktop hands `hermes update` the system proxy in both spellings (HTTPS_PROXY and https_proxy). The .env load replaces only the spelling the file uses, and urllib and curl prefer the lower-case one, so the update still went to the system proxy and the .env NO_PROXY stopped applying. After the .env load, `hermes update` now undoes everything the Desktop added when .env sets a proxy (the Desktop injects nothing when it sees one). A .env NO_PROXY alone replaces the user own entries in every injected NO_PROXY variant and keeps the bypass the Desktop added; a `*` there drops the injection. Review S1 (kshitijk4poor) on #144894.
81 lines
4.0 KiB
Python
81 lines
4.0 KiB
Python
"""Undo the system proxy the Desktop handed to an update's children.
|
|
|
|
The Desktop resolves the OS proxy (PAC included) and gives it to the update's
|
|
git, urllib, npm and uv children as HTTPS_PROXY/HTTP_PROXY with an extended
|
|
NO_PROXY (apps/desktop/electron/updater/system-proxy-env.ts). ``hermes update``
|
|
then restarts gateways and backends that inherit that env; for them it is not
|
|
the user's setting, and every host the PAC sends direct (LAN model servers,
|
|
internal names) would go through the proxy. ``HERMES_UPDATE_SYSTEM_PROXY`` names
|
|
the variables the Desktop added and ``HERMES_UPDATE_USER_<KEY>`` holds a user's
|
|
own NO_PROXY it extended; a long-lived process restores its env from them at
|
|
start. Same algorithm as restoreUserProxyEnv, posix.sh and windows.ps1, plus one
|
|
rule only a Hermes process needs: a value the user's ``.env`` set wins. ``hermes
|
|
update`` and the restarted process load ``~/.hermes/.env`` with override before
|
|
this runs, so a marked key holding the value a dotenv load published is the
|
|
user's own setting (same value as the Desktop's or not) and is kept. The Desktop
|
|
and the hand-off scripts never load ``.env``, so their envs never hit that case.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import MutableMapping
|
|
|
|
MARKER = "HERMES_UPDATE_SYSTEM_PROXY"
|
|
SAVED_PREFIX = "HERMES_UPDATE_USER_"
|
|
_ADDED_KEYS = ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "NO_PROXY", "no_proxy")
|
|
_NO_PROXY_KEYS = ("NO_PROXY", "no_proxy")
|
|
|
|
|
|
def restore_user_proxy_env(environ: MutableMapping[str, str] | None = None) -> None:
|
|
"""Drop the proxy variables the Desktop added and restore the user's own NO_PROXY."""
|
|
from hermes_cli.env_loader import dotenv_published_values
|
|
|
|
env = os.environ if environ is None else environ
|
|
|
|
def users_dotenv_value(key: str) -> bool:
|
|
return key in env and env[key] in dotenv_published_values(key)
|
|
|
|
for key in env.get(MARKER, "").split(","):
|
|
if key in _ADDED_KEYS and not users_dotenv_value(key):
|
|
env.pop(key, None)
|
|
for key in _NO_PROXY_KEYS:
|
|
saved = env.pop(SAVED_PREFIX + key, None)
|
|
if saved is not None and not users_dotenv_value(key):
|
|
env[key] = saved
|
|
env.pop(MARKER, None)
|
|
|
|
|
|
def prefer_dotenv_proxy_env(environ: MutableMapping[str, str] | None = None) -> None:
|
|
"""In ``hermes update``, after its ``.env`` load: the user's ``.env`` proxy beats the system one.
|
|
|
|
The Desktop injects every case variant (``HTTPS_PROXY`` and ``https_proxy``) but the ``.env``
|
|
load replaces only the spelling the file uses, and urllib and curl prefer the lower-case one, so
|
|
the update would still go through the system proxy. A ``.env`` proxy is an explicit setting the
|
|
Desktop could not see; had it seen one it would have injected nothing, so everything it added is
|
|
undone. A ``.env`` NO_PROXY alone replaces the user's own entries in every injected NO_PROXY
|
|
variant, keeping the entries the Desktop added; a ``*`` in it bypasses every proxy.
|
|
"""
|
|
from agent.proxy_bypass import no_proxy_entries
|
|
from hermes_cli.env_loader import dotenv_published_values
|
|
|
|
env = os.environ if environ is None else environ
|
|
if MARKER not in env:
|
|
return
|
|
if any(dotenv_published_values(name) for name in ("https_proxy", "http_proxy", "all_proxy")):
|
|
restore_user_proxy_env(env)
|
|
return
|
|
published = dotenv_published_values("no_proxy")
|
|
users = next((env[key] for key in env if key.lower() == "no_proxy" and env[key] in published), None)
|
|
if users is None:
|
|
return
|
|
if "*" in no_proxy_entries(users):
|
|
restore_user_proxy_env(env)
|
|
return
|
|
own = {e for key in _NO_PROXY_KEYS for e in no_proxy_entries(env.get(SAVED_PREFIX + key, ""))}
|
|
added = [e for key in _NO_PROXY_KEYS if env.get(key, users) != users
|
|
for e in no_proxy_entries(env[key]) if e not in own]
|
|
merged = ",".join(dict.fromkeys([*no_proxy_entries(users), *added]))
|
|
for key in _NO_PROXY_KEYS:
|
|
if key in env:
|
|
env[key] = merged
|