mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(auth): restore --label for hermes auth add nous --type oauth
persist_nous_credentials() now accepts an optional label kwarg which
gets embedded in providers.nous under the 'label' key.
_seed_from_singletons() prefers the embedded label over the
auto-derived label_from_token() fingerprint when materialising the
pool entry, so re-seeding on every load_pool('nous') preserves the
user's chosen label.
auth_commands.py threads --label through to the helper, restoring
parity with how other OAuth providers (anthropic, codex, google,
qwen) honor the flag.
Tests: 4 new (embed, reseed-survives, no-label fallback, end-to-end
through auth_add_command). All 390 nous/auth/credential_pool tests
pass.
This commit is contained in:
parent
c7fece1f9d
commit
2297c5f5ce
5 changed files with 170 additions and 4 deletions
|
|
@ -168,6 +168,67 @@ def test_auth_add_nous_oauth_persists_pool_entry(tmp_path, monkeypatch):
|
|||
assert singleton["inference_base_url"] == "https://inference.example.com/v1"
|
||||
|
||||
|
||||
def test_auth_add_nous_oauth_honors_custom_label(tmp_path, monkeypatch):
|
||||
"""`hermes auth add nous --type oauth --label <name>` must preserve the
|
||||
custom label end-to-end — it was silently dropped in the first cut of the
|
||||
persist_nous_credentials helper because `--label` wasn't threaded through.
|
||||
"""
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
||||
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
|
||||
token = _jwt_with_email("nous@example.com")
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._nous_device_code_login",
|
||||
lambda **kwargs: {
|
||||
"portal_base_url": "https://portal.example.com",
|
||||
"inference_base_url": "https://inference.example.com/v1",
|
||||
"client_id": "hermes-cli",
|
||||
"scope": "inference:mint_agent_key",
|
||||
"token_type": "Bearer",
|
||||
"access_token": token,
|
||||
"refresh_token": "refresh-token",
|
||||
"obtained_at": "2026-03-23T10:00:00+00:00",
|
||||
"expires_at": "2026-03-23T11:00:00+00:00",
|
||||
"expires_in": 3600,
|
||||
"agent_key": "ak-test",
|
||||
"agent_key_id": "ak-id",
|
||||
"agent_key_expires_at": "2026-03-23T10:30:00+00:00",
|
||||
"agent_key_expires_in": 1800,
|
||||
"agent_key_reused": False,
|
||||
"agent_key_obtained_at": "2026-03-23T10:00:10+00:00",
|
||||
"tls": {"insecure": False, "ca_bundle": None},
|
||||
},
|
||||
)
|
||||
|
||||
from hermes_cli.auth_commands import auth_add_command
|
||||
|
||||
class _Args:
|
||||
provider = "nous"
|
||||
auth_type = "oauth"
|
||||
api_key = None
|
||||
label = "my-nous"
|
||||
portal_url = None
|
||||
inference_url = None
|
||||
client_id = None
|
||||
scope = None
|
||||
no_browser = False
|
||||
timeout = None
|
||||
insecure = False
|
||||
ca_bundle = None
|
||||
|
||||
auth_add_command(_Args())
|
||||
|
||||
payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
|
||||
|
||||
# Custom label reaches the pool entry …
|
||||
pool_entry = payload["credential_pool"]["nous"][0]
|
||||
assert pool_entry["source"] == "device_code"
|
||||
assert pool_entry["label"] == "my-nous"
|
||||
|
||||
# … and survives in providers.nous so a subsequent load_pool() re-seeds
|
||||
# it without reverting to the auto-derived fingerprint.
|
||||
assert payload["providers"]["nous"]["label"] == "my-nous"
|
||||
|
||||
|
||||
def test_auth_add_codex_oauth_persists_pool_entry(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
||||
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
|
||||
|
|
|
|||
|
|
@ -633,3 +633,81 @@ def test_persist_nous_credentials_reloads_pool_after_singleton_write(tmp_path, m
|
|||
# assert its exact value, just that the helper returned a real entry.
|
||||
assert entry.access_token == "access-tok"
|
||||
assert entry.agent_key == "agent-key-value"
|
||||
|
||||
|
||||
def test_persist_nous_credentials_embeds_custom_label(tmp_path, monkeypatch):
|
||||
"""User-supplied ``--label`` round-trips through providers.nous and the pool.
|
||||
|
||||
Previously `hermes auth add nous --type oauth --label <name>` silently
|
||||
dropped the label because persist_nous_credentials() ignored it and
|
||||
_seed_from_singletons always auto-derived via label_from_token(). The
|
||||
fix stashes the label inside providers.nous so seeding prefers it.
|
||||
"""
|
||||
from hermes_cli.auth import persist_nous_credentials, NOUS_DEVICE_CODE_SOURCE
|
||||
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
(hermes_home / "auth.json").write_text(json.dumps({
|
||||
"version": 1, "providers": {},
|
||||
}))
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
entry = persist_nous_credentials(_full_state_fixture(), label="my-personal")
|
||||
assert entry is not None
|
||||
assert entry.source == NOUS_DEVICE_CODE_SOURCE
|
||||
assert entry.label == "my-personal"
|
||||
|
||||
# providers.nous carries the label so re-seeding on the next load_pool
|
||||
# doesn't overwrite it with the auto-derived fingerprint.
|
||||
payload = json.loads((hermes_home / "auth.json").read_text())
|
||||
assert payload["providers"]["nous"]["label"] == "my-personal"
|
||||
|
||||
|
||||
def test_persist_nous_credentials_custom_label_survives_reseed(tmp_path, monkeypatch):
|
||||
"""Reopening the pool (which re-runs _seed_from_singletons) must keep the
|
||||
user-chosen label instead of clobbering it with label_from_token output.
|
||||
"""
|
||||
from hermes_cli.auth import persist_nous_credentials
|
||||
from agent.credential_pool import load_pool
|
||||
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
(hermes_home / "auth.json").write_text(json.dumps({
|
||||
"version": 1, "providers": {},
|
||||
}))
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
persist_nous_credentials(_full_state_fixture(), label="work-acct")
|
||||
|
||||
# Second load_pool triggers _seed_from_singletons again. Without the
|
||||
# fix, this call overwrote the label with label_from_token(access_token).
|
||||
pool = load_pool("nous")
|
||||
entries = pool.entries()
|
||||
assert len(entries) == 1
|
||||
assert entries[0].label == "work-acct"
|
||||
|
||||
|
||||
def test_persist_nous_credentials_no_label_uses_auto_derived(tmp_path, monkeypatch):
|
||||
"""When the caller doesn't pass ``label``, the auto-derived fingerprint
|
||||
is used (unchanged default behaviour — regression guard).
|
||||
"""
|
||||
from hermes_cli.auth import persist_nous_credentials
|
||||
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
(hermes_home / "auth.json").write_text(json.dumps({
|
||||
"version": 1, "providers": {},
|
||||
}))
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
entry = persist_nous_credentials(_full_state_fixture())
|
||||
assert entry is not None
|
||||
# label_from_token derives from the access_token; exact value depends on
|
||||
# the fingerprinter but it must not be empty and must not equal an
|
||||
# arbitrary user string we never passed.
|
||||
assert entry.label
|
||||
assert entry.label != "my-personal"
|
||||
|
||||
# No "label" key embedded in providers.nous when the caller didn't supply one.
|
||||
payload = json.loads((hermes_home / "auth.json").read_text())
|
||||
assert "label" not in payload["providers"]["nous"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue