merge origin/main (779 commits) into ethie/pm-clean
Branch semantics kept where main and PM disagree: update_cmd_deps.py, constraints-termux.txt, the Electron update-api-check module and the post-swap hand-off test stay deleted; the pending-fleet-restart catch-up and the local_runtime tag/download ladder stay retired (PM owns engines). Ported from main onto the branch's shape: profile_scoped_chore for the auto-archive and plugin-update housekeeping chores, the local-runtime cross-process boot lock and residency cap, the checkpoint tmp_pack sweep, the cua daemon-liveness status probe, the remote-served Desktop update flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways (urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn without [standard]), and the umask-scoping spawn test. uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from main switched to utf-8-sig (check-windows-footguns).
This commit is contained in:
@@ -173,3 +173,45 @@ def test_post_discovery_registration_is_mirrored(_isolated_registries, monkeypat
|
||||
)
|
||||
assert LATE in auth_mod.PROVIDER_REGISTRY
|
||||
assert auth_mod.PROVIDER_REGISTRY[LATE_ALIAS] is auth_mod.PROVIDER_REGISTRY[LATE]
|
||||
|
||||
|
||||
def test_user_plugin_alias_repoints_and_display_name_follows(_isolated_registries, monkeypatch, tmp_path):
|
||||
"""A $HERMES_HOME plugin owns the aliases it declares and the display name of a same-name row.
|
||||
|
||||
Ownership split (#116668): ``providers.get_provider_profile`` already followed the user's
|
||||
profile for the alias, while the auth registry kept the alias on whichever row got there first
|
||||
and kept the bundled display name on a same-name replacement.
|
||||
"""
|
||||
monkeypatch.setattr(providers, "_discover_entry_point_providers", lambda: None)
|
||||
monkeypatch.setattr(providers, "_BUNDLED_PLUGINS_DIR", tmp_path)
|
||||
monkeypatch.setattr(providers, "_user_plugins_dir", lambda: None)
|
||||
monkeypatch.setattr(providers, "_installed_plugins_dir", lambda: None)
|
||||
providers._discover_providers()
|
||||
|
||||
taken_alias = "probe-116668-alias"
|
||||
monkeypatch.setattr(providers, "_current_source", "bundled")
|
||||
providers.register_provider(ProviderProfile(
|
||||
name="probe-116668-bundled", display_name="Bundled", base_url="https://bundled.example/v1",
|
||||
env_vars=("PROBE_116668_BUNDLED_KEY",), aliases=(taken_alias,)))
|
||||
bundled_row = auth_mod.PROVIDER_REGISTRY["probe-116668-bundled"]
|
||||
assert auth_mod.PROVIDER_REGISTRY[taken_alias] is bundled_row
|
||||
|
||||
# A second bundled plugin claiming the same alias does not steal it.
|
||||
providers.register_provider(ProviderProfile(
|
||||
name="probe-116668-other", display_name="Other", base_url="https://other.example/v1",
|
||||
env_vars=("PROBE_116668_OTHER_KEY",), aliases=(taken_alias,)))
|
||||
assert auth_mod.PROVIDER_REGISTRY[taken_alias] is bundled_row
|
||||
|
||||
# The user's plugin does, and its same-name replacement rewrites the display name in place.
|
||||
monkeypatch.setattr(providers, "_current_source", "user")
|
||||
providers.register_provider(ProviderProfile(
|
||||
name="probe-116668-user", display_name="Mine", base_url="https://mine.example/v1",
|
||||
env_vars=("PROBE_116668_USER_KEY",), aliases=(taken_alias,)))
|
||||
assert auth_mod.PROVIDER_REGISTRY[taken_alias] is auth_mod.PROVIDER_REGISTRY["probe-116668-user"]
|
||||
|
||||
providers.register_provider(ProviderProfile(
|
||||
name="probe-116668-bundled", display_name="Bundled (mine)", base_url="https://mine.example/v2",
|
||||
env_vars=("PROBE_116668_BUNDLED_KEY",), aliases=(taken_alias,)))
|
||||
assert bundled_row.name == "Bundled (mine)"
|
||||
assert bundled_row.inference_base_url == "https://mine.example/v2"
|
||||
assert auth_mod.PROVIDER_REGISTRY[taken_alias] is bundled_row
|
||||
|
||||
@@ -105,3 +105,60 @@ def test_token_url_off_authorize_allowlist_refused_before_any_request(idp, monke
|
||||
assert {login_err.value.code, refresh_err.value.code} == {"oauth_token_host_rejected"}
|
||||
with pytest.raises(AuthError, match="HTTPS"):
|
||||
pkce.validate_config(PROVIDER, _config(idp, authorize_url="http://auth.example.com/authorize"))
|
||||
|
||||
|
||||
def test_spent_refresh_token_is_grant_dead_and_marks_the_pool_row_dead(idp, monkeypatch, tmp_path, request):
|
||||
"""RFC 6749 invalid_grant on refresh is terminal. The pool must DEAD the row, not EXHAUST it."""
|
||||
from agent.credential_pool import STATUS_DEAD, load_pool
|
||||
|
||||
monkeypatch.setattr(pkce.webbrowser, "open", _browser_hits)
|
||||
monkeypatch.setattr("hermes_cli.auth_device_flow._can_open_graphical_browser", lambda: True)
|
||||
cfg = _config(idp)
|
||||
handler, refresh = pkce.pkce_auth_handler(cfg), pkce.pkce_refresh_credential(cfg)
|
||||
register_provider(ProviderProfile(name=PROVIDER, auth_type="oauth_external",
|
||||
base_url="https://example.invalid/v1",
|
||||
auth_handler=handler, refresh_credential=refresh))
|
||||
request.addfinalizer(lambda: (providers._REGISTRY.pop(PROVIDER, None),
|
||||
hermes_cli.auth.PROVIDER_REGISTRY.pop(PROVIDER, None)))
|
||||
args = SimpleNamespace(provider=PROVIDER, no_browser=False)
|
||||
assert handler("add", args) is True
|
||||
rows = json.loads((tmp_path / "hermes" / "auth.json").read_text())["credential_pool"][PROVIDER]
|
||||
spent = rows[0]["refresh_token"]
|
||||
idp.refresh_tokens.discard(spent)
|
||||
|
||||
with pytest.raises(AuthError) as excinfo:
|
||||
refresh(SimpleNamespace(provider=PROVIDER, id=rows[0]["id"], refresh_token=spent,
|
||||
access_token=rows[0]["access_token"], expires_at_ms=None))
|
||||
assert excinfo.value.code == "invalid_grant"
|
||||
|
||||
pool = load_pool(PROVIDER)
|
||||
result = pool.try_refresh_matching(credential_id=rows[0]["id"])
|
||||
assert result is None or result.last_status == STATUS_DEAD
|
||||
disk = json.loads((tmp_path / "hermes" / "auth.json").read_text())["credential_pool"][PROVIDER][0]
|
||||
assert disk["last_status"] == STATUS_DEAD
|
||||
|
||||
|
||||
def test_alias_login_stores_the_row_under_the_canonical_profile_name(idp, monkeypatch, tmp_path, request):
|
||||
"""hermes auth add <alias> must write the pool under ProviderProfile.name, not the typed alias."""
|
||||
from agent.credential_pool import load_pool
|
||||
|
||||
alias = "example-pkce-alias"
|
||||
monkeypatch.setattr(pkce.webbrowser, "open", _browser_hits)
|
||||
monkeypatch.setattr("hermes_cli.auth_device_flow._can_open_graphical_browser", lambda: True)
|
||||
cfg = _config(idp)
|
||||
handler = pkce.pkce_auth_handler(cfg)
|
||||
register_provider(ProviderProfile(
|
||||
name=PROVIDER, aliases=(alias,), auth_type="oauth_external",
|
||||
base_url="https://example.invalid/v1",
|
||||
auth_handler=handler, refresh_credential=pkce.pkce_refresh_credential(cfg)))
|
||||
request.addfinalizer(lambda: (
|
||||
providers._REGISTRY.pop(PROVIDER, None),
|
||||
providers._ALIASES.pop(alias, None),
|
||||
hermes_cli.auth.PROVIDER_REGISTRY.pop(PROVIDER, None),
|
||||
hermes_cli.auth.PROVIDER_REGISTRY.pop(alias, None)))
|
||||
args = SimpleNamespace(provider=alias, no_browser=False)
|
||||
assert handler("add", args) is True
|
||||
pool = json.loads((tmp_path / "hermes" / "auth.json").read_text())["credential_pool"]
|
||||
assert PROVIDER in pool and alias not in pool
|
||||
assert handler("status", args) is True
|
||||
assert load_pool(PROVIDER).entries()[0].provider == PROVIDER
|
||||
|
||||
Reference in New Issue
Block a user