mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(dashboard): provider device-login polls run in the session's profile
The provider-profile poller ran poll_device_login on a background thread whose context was copied after the request's ?profile= scope ended, so poll hooks read the launch home's config and a hosted profile polled through the launch home's plugin host (or failed when only the requested profile used host isolation). Each poll now runs under _config_profile_scope of the session's profile: the await-safe scope (home + secret scope), never the skills-module lock, so nothing is held across the wait between polls.
This commit is contained in:
1 parent
8d359c7824
commit
0fcbd6561e
2 files changed
+16
-5
No files matched your search
@@ -357,15 +357,16 @@ _DEVICE_LOGIN_ENDINGS = {"denied": "Authorization was denied.", "expired": "Devi
|
||||
@_oauth_poller("provider-profile")
|
||||
def _profile_device_poller(session_id: str, sess: dict[str, Any]) -> None:
|
||||
"""Drive a device-code login a provider profile owns (``ProviderProfile.start_device_login``):
|
||||
one ``poll_device_login`` per interval until approved, then ``save_device_login`` under the
|
||||
session's profile scope, in one critical section with the cancel check like the built-in savers."""
|
||||
from hermes_cli.web_server_profiles import _profile_scope
|
||||
one ``poll_device_login`` per interval (each bound to the session's profile) until approved, then
|
||||
``save_device_login`` under the session's profile scope, in one critical section with the cancel check like the built-in savers."""
|
||||
from hermes_cli.web_server_profiles import _config_profile_scope, _profile_scope
|
||||
profile, state, interval = sess["_provider_profile"], sess["_device_state"], int(sess["interval"])
|
||||
result: dict[str, Any] = {}
|
||||
while time.time() < sess["expires_at"]:
|
||||
if _sleep_unless_cancelled(sess, interval):
|
||||
break
|
||||
result = dict(profile.poll_device_login(state) or {})
|
||||
with _config_profile_scope(sess.get("profile")): # await-safe: no skills lock across the wait
|
||||
result = dict(profile.poll_device_login(state) or {})
|
||||
state = result.get("state", state)
|
||||
if result.get("status") != "pending":
|
||||
break
|
||||
|
||||
@@ -40,8 +40,9 @@ def start():
|
||||
"expires_in": 60, "interval": 1, "state": {"polls": 0}}
|
||||
|
||||
def poll(state):
|
||||
from hermes_constants import get_hermes_home
|
||||
polls = state["polls"] + 1
|
||||
_log("poll", polls=polls)
|
||||
_log("poll", polls=polls, home=str(get_hermes_home()))
|
||||
if polls < int(os.environ.get("ACME_APPROVE_AT", "2")):
|
||||
return {"status": "pending", "state": {"polls": polls}}
|
||||
if os.environ.get("ACME_DENY"):
|
||||
@@ -133,6 +134,15 @@ def test_profile_device_login_lists_starts_and_saves_in_the_owning_profile(acme)
|
||||
assert [e["polls"] for e in events() if e["event"] == "poll"] == [1, 2]
|
||||
|
||||
|
||||
def test_profile_device_login_polls_in_the_owning_profile(acme):
|
||||
"""The background poller binds the session's ``?profile=`` home around every poll, so a hook that
|
||||
reads config (or a hosted profile picking its plugin host) sees coder's home, not the launch one."""
|
||||
coder, events = acme
|
||||
body = client.post(f"/api/providers/oauth/{NAME}/start?profile=coder", headers=HEADERS).json()
|
||||
assert _wait(body["session_id"], "coder")["status"] == "approved"
|
||||
assert [e["home"] for e in events() if e["event"] == "poll"] == [str(coder), str(coder)]
|
||||
|
||||
|
||||
def test_profile_device_login_cancel_before_approval_never_saves(acme, monkeypatch):
|
||||
_coder, events = acme
|
||||
monkeypatch.setenv("ACME_APPROVE_AT", "1") # the very first poll would approve and save
|
||||
|
||||
Reference in new issue
Block a user