Skip to content

Daemon credential store - #1367

Open
alozowski wants to merge 4 commits into
mainfrom
feat/1366-daemon-credential-store
Open

alozowski wants to merge 4 commits into
mainfrom
feat/1366-daemon-credential-store

Conversation

@alozowski

@alozowski alozowski commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Issue

Closes #1366

Description

  • first of all, all the dead code in hf_auth.py was removed;
  • the daemon now stores its HF credentials in a dedicated file with 0600 permissions (mkstemp() in _write_store() function). Manual token entry, redirect OAuth, and device-code OAuth all write to this store, and every daemon credential read uses it;
  • sign-out writes a signed-out record and advances a lifecycle generation instead of calling logout(). This prevents an in-flight login from restoring credentials after sign-out while leaving the user’s CLI credentials untouched. Cancelling an OAuth session is also final: cancellation and credential persistence are coordinated so a late token response cannot be saved;
  • all HF calls now choose their credentials explicitly. Private resources use the daemon-owned token, while the public emotions, dances, and face-detector repositories explicitly disable authentication. The daemon therefore never inherits credentials from HF_TOKEN or hf auth login;
  • the central relay uses the same daemon-owned credentials and refreshes them after a 401, allowing the robot to reconnect without restarting;
  • there is intentionally no migration from the Hugging Face CLI token file or HF_TOKEN.

Note, that after upgrading, an already linked robot appears signed out once and must be linked again.

Testing

Tested on a wireless against live Hugging Face. Starting from signed out, a desktop-app login reached "Remote access enabled". The new file held a refresh token with a 30-day expiry at mode 600. The unit's existing token file wasn't read and stayed unchanged.

Tested on

  • Reachy Mini Wireless
  • Reachy Mini Lite
  • MuJoCo simulation (--sim)
  • Mockup simulation (--mockup-sim)
  • Not applicable (e.g. docs, typo)

AI assistance

  • No AI involvement
  • AI helped with wording or boilerplate
  • AI wrote code here, and I ran and reviewed it
  • An agent produced this PR, and I have not run it myself

Comment on lines -21 to -35
# =============================================================================
# OAuth Configuration
# =============================================================================
# Register ONE OAuth app at https://huggingface.co/settings/connected-applications
# with TWO redirect URIs:
# - http://reachy-mini.local:8000/api/hf-auth/oauth/callback (wireless)
# - http://localhost:8000/api/hf-auth/oauth/callback (lite)
#
# Then set HF_OAUTH_CLIENT_ID on all robots (same value for all).
#
# Environment variables:
# HF_OAUTH_CLIENT_ID - Required for OAuth login
# HF_OAUTH_CLIENT_SECRET - Optional (for confidential clients)
#
# Pollen's HuggingFace OAuth app - works for all Reachy Mini robots

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I would suggest better to keep the comments for better understanding what it does of following section. If any behavior changed then just sync the comments as well. thanks.

@FabienDanieau FabienDanieau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on a Wireless (1.10.0 + live HF) and the design holds up: store lands at 0600 with a 30-day refresh token, sign-out leaves the user's token/stored_tokens byte-identical while the daemon refuses to read them (verified with a poisoned HF_TOKEN too), and the 401 refresh really does reconnect where main never does. Nice work, this file needed the rewrite.

Requesting changes for the rebase fallout, not the design.

Rebase needed. Branch is CONFLICTING with main; conflicts are test-only (test_hf_auth.py, test_router_hf_auth.py, test_webrtc_utils.py), all keep-both except test_hf_auth.py where main's token_path.read_text() / replacement_observation asserts have to go, since that's exactly the behaviour this PR removes.

28 tests fail once rebased (baseline main has 1 pre-existing failure in test_wireless.py). Two causes, both mechanical:

  • test_trusted_endpoints.py:28 patches huggingface_hub.get_token, but webrtc_utils._refresh_once now reads hf_auth.get_hf_token. The patch lands nowhere, the daemon sees signed-out, TURN never fetches, and 24 TURN/relay tests time out. Point the fixture at hf_auth.get_hf_token and they pass.
  • Your new test_turn_credentials_refresh_once_populates_cache stub _Resp has no status_code; main's _refresh_once reads it since #1365. Add status_code = 200.

ruff format rewrites 4 spots in hf_auth.py (three collapsed logger.debug(...) calls and _complete_device_login's signature). It's a pre-commit hook, so lint CI goes red.

Worth a look, not blocking: get_hf_credential holds _store_lock across the refresh_access_token() network call, and get_hf_token() is called synchronously from async def routes, including /api/hf-auth/status which the apps poll. Inside the 5-minute refresh margin a status poll stalls the event loop on an HTTPS round-trip and parks the relay and TURN threads on the same lock. get_token() used to be a file read. The generation counter you already have is the CAS you need: snapshot under the lock, release, refresh, re-acquire and only write if lifecycle_generation hasn't moved.

Minor: the immediate retry after a recovered 401 is wasted (_token_updated stays set into the next cycle, watcher tears it down at +20ms, reconnect lands after a full backoff ~5s). Bumping _connection_attempts and returning instead of .set() gets it to ~50ms. Measured on the robot; happy to share the harness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The daemon stores its Hugging Face credentials in the user's own credential store

3 participants