fix(auth): per-profile Anthropic OAuth file + complete port-binding platform set (#57563 salvage) (#59339)
* fix(auth): resolve Anthropic OAuth file per-profile + close port-binding platform gaps Two focused pieces salvaged from PR #57563: 1. _HERMES_OAUTH_FILE was computed at module import time — frozen before HERMES_HOME/profile overrides, so multiplexed profile turns read and wrote the DEFAULT profile's .anthropic_oauth.json (OAuth path hijack). Replaced with a lazy _get_hermes_oauth_file(); all web_server.py call sites updated. 2. _PORT_BINDING_PLATFORM_VALUES was missing whatsapp_cloud and line — both bind aiohttp TCP listeners, so a secondary multiplex profile enabling them would collide with the primary's listener instead of failing fast at startup. Original work by @austinlaw076. The rest of #57563 was redundant on main (adapter routing sweep superseded by #56854's salvage; cron secret scope landed in fdab380a1; nested-config fallback in from_dict). * chore(release): map austinlaw076 author email for PR #57563 salvage * test(hermes_cli): patch _get_hermes_oauth_file instead of removed _HERMES_OAUTH_FILE constant --------- Co-authored-by: Austin <austin@openvm067.space> Co-authored-by: Ben <ben@nousresearch.com>fix/verification-admin-route-recovery
parent
249c69b958
commit
76979a0869
|
|
@ -1390,7 +1390,8 @@ _OAUTH_TOKEN_URL = _OAUTH_TOKEN_URLS[0]
|
|||
_OAUTH_TOKEN_USER_AGENT = "axios/1.7.9"
|
||||
_OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
|
||||
_OAUTH_SCOPES = "org:create_api_key user:profile user:inference"
|
||||
_HERMES_OAUTH_FILE = get_hermes_home() / ".anthropic_oauth.json"
|
||||
def _get_hermes_oauth_file() -> Path:
|
||||
return get_hermes_home() / ".anthropic_oauth.json"
|
||||
|
||||
|
||||
def _generate_pkce() -> tuple:
|
||||
|
|
@ -1538,9 +1539,10 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
|
|||
|
||||
def read_hermes_oauth_credentials() -> Optional[Dict[str, Any]]:
|
||||
"""Read Hermes-managed OAuth credentials from ~/.hermes/.anthropic_oauth.json."""
|
||||
if _HERMES_OAUTH_FILE.exists():
|
||||
oauth_file = _get_hermes_oauth_file()
|
||||
if oauth_file.exists():
|
||||
try:
|
||||
data = json.loads(_HERMES_OAUTH_FILE.read_text(encoding="utf-8"))
|
||||
data = json.loads(oauth_file.read_text(encoding="utf-8"))
|
||||
if data.get("accessToken"):
|
||||
return data
|
||||
except (json.JSONDecodeError, OSError, IOError) as e:
|
||||
|
|
|
|||
|
|
@ -1363,6 +1363,8 @@ _PORT_BINDING_PLATFORM_VALUES = frozenset({
|
|||
"wecom_callback",
|
||||
"bluebubbles",
|
||||
"sms",
|
||||
"whatsapp_cloud",
|
||||
"line",
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6443,11 +6443,11 @@ def _anthropic_oauth_status() -> Dict[str, Any]:
|
|||
try:
|
||||
from agent.anthropic_adapter import (
|
||||
read_hermes_oauth_credentials,
|
||||
_HERMES_OAUTH_FILE,
|
||||
_get_hermes_oauth_file,
|
||||
)
|
||||
except ImportError:
|
||||
read_hermes_oauth_credentials = None # type: ignore
|
||||
_HERMES_OAUTH_FILE = None # type: ignore
|
||||
_get_hermes_oauth_file = None # type: ignore
|
||||
|
||||
hermes_creds = None
|
||||
if read_hermes_oauth_credentials:
|
||||
|
|
@ -6459,7 +6459,7 @@ def _anthropic_oauth_status() -> Dict[str, Any]:
|
|||
return {
|
||||
"logged_in": True,
|
||||
"source": "hermes_pkce",
|
||||
"source_label": f"Hermes PKCE ({_HERMES_OAUTH_FILE})",
|
||||
"source_label": f"Hermes PKCE ({_get_hermes_oauth_file() if _get_hermes_oauth_file else None})",
|
||||
"token_preview": _truncate_token(hermes_creds.get("accessToken")),
|
||||
"expires_at": hermes_creds.get("expiresAt"),
|
||||
"has_refresh_token": bool(hermes_creds.get("refreshToken")),
|
||||
|
|
@ -6896,9 +6896,10 @@ async def disconnect_oauth_provider(
|
|||
if provider_id == "anthropic":
|
||||
cleared = False
|
||||
try:
|
||||
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
|
||||
if _HERMES_OAUTH_FILE.exists():
|
||||
_HERMES_OAUTH_FILE.unlink()
|
||||
from agent.anthropic_adapter import _get_hermes_oauth_file
|
||||
oauth_file = _get_hermes_oauth_file()
|
||||
if oauth_file.exists():
|
||||
oauth_file.unlink()
|
||||
cleared = True
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -7042,24 +7043,25 @@ def _save_anthropic_oauth_creds(access_token: str, refresh_token: str, expires_a
|
|||
Mirrors what auth_commands.add_command does so the dashboard flow leaves
|
||||
the system in the same state as ``hermes auth add anthropic``.
|
||||
"""
|
||||
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
|
||||
from agent.anthropic_adapter import _get_hermes_oauth_file
|
||||
oauth_file = _get_hermes_oauth_file()
|
||||
payload = {
|
||||
"accessToken": access_token,
|
||||
"refreshToken": refresh_token,
|
||||
"expiresAt": expires_at_ms,
|
||||
}
|
||||
_HERMES_OAUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = _HERMES_OAUTH_FILE.with_name(
|
||||
f"{_HERMES_OAUTH_FILE.name}.tmp.{os.getpid()}.{secrets.token_hex(8)}"
|
||||
oauth_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = oauth_file.with_name(
|
||||
f"{oauth_file.name}.tmp.{os.getpid()}.{secrets.token_hex(8)}"
|
||||
)
|
||||
try:
|
||||
with tmp_path.open("w", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload, indent=2))
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(tmp_path, _HERMES_OAUTH_FILE)
|
||||
os.replace(tmp_path, oauth_file)
|
||||
try:
|
||||
_HERMES_OAUTH_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||||
oauth_file.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ AUTHOR_MAP = {
|
|||
"AndreasHiltner@users.noreply.github.com": "AndreasHiltner", # PR #56854 salvage (gateway: route multiplex profile responses through the profile's own adapter — 53-site _adapter_for_source sweep)
|
||||
"allenliang2022@users.noreply.github.com": "allenliang2022", # PR #56932 test coverage folded into #56909 salvage (408 → retryable timeout)
|
||||
"m888.braun@hotmail.com": "ManniBr", # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles)
|
||||
"austin@openvm067.space": "austinlaw076", # PR #57563 partial salvage (auth: lazy per-profile Anthropic OAuth file; gateway: whatsapp_cloud/line added to port-binding platform set)
|
||||
"blueirobin02@gmail.com": "irresi", # PR #59048 salvage (gateway: scope reset banners' session info to the serving profile; #59003)
|
||||
"jashlee+microsoft@microsoft.com": "s905060", # PR #57943 salvage (photon: auto-reinstall stale sidecar node_modules when lockfile is newer than npm's install marker; #59169)
|
||||
"lohinth25@proton.me": "l0h1nth", # PR #32210 salvage (mattermost: accept leading-space slash commands from mobile clients; #25184)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class _DummyPool:
|
|||
@pytest.fixture
|
||||
def oauth_file(monkeypatch, tmp_path):
|
||||
target = tmp_path / '.anthropic_oauth.json'
|
||||
monkeypatch.setattr('agent.anthropic_adapter._HERMES_OAUTH_FILE', target)
|
||||
monkeypatch.setattr('agent.anthropic_adapter._get_hermes_oauth_file', lambda: target)
|
||||
monkeypatch.setattr('agent.credential_pool.load_pool', lambda _provider: _DummyPool())
|
||||
return target
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue