10 KiB
| name | description | type |
|---|---|---|
| Per-user agent registration keys | Replace the shared [wac] API_KEY with per-user registration keys issued from the RIA Agents page on RIA Hub. | plan |
Per-user agent registration keys — plan
Status: design only; nothing implemented.
Owner (toolkit side): ria-toolkit-oss
Owner (hub side): ria-hub / controller
Related: screens_agent_handoff.md, agent_tx_protocol.md
Context (current state)
Today, ria-agent register calls POST {hub_url}/screens/agents/register with
an X-API-Key header (cli.py:41-64).
The hub validates that header against a single shared secret — [wac] API_KEY
in the hub's app.ini (legacy_executor.py:821-822).
The hub responds with {agent_id, token}; the agent persists both to
~/.ria/agent.json and uses token as the bearer on the WS handshake
afterwards.
Consequences of the shared secret:
- Every agent operator holds the same key → no per-user attribution in logs.
- Revoking one operator forces a rotation across every deployed agent.
- Key-in-CLI-history leaks escalate to the whole fleet.
- Nothing ties a registered agent to a human in the hub's user table.
Goal
A user signs into riahub.ai, opens an RIA Agents page, mints a key, and
uses it once with ria-agent register. The resulting agent is owned by that
user; the key can be revoked without affecting anyone else's agents.
The agent-side token returned by /screens/agents/register keeps its current
role (bearer for the WS handshake). Only the registration credential
changes.
User flow
- User signs into
https://riahub.ai. - User navigates to Settings → RIA Agents (or a top-level
/agentspage — see open question O1). - User clicks Generate registration key. A modal shows the key once, with copy-to-clipboard. Only a prefix + hash is stored server-side.
- User runs, on the agent host:
ria-agent register --hub https://riahub.ai --api-key ria_reg_<...> - Hub validates the key, creates an agent row owned by the user, marks the
key as
consumed(one-shot) or bumpslast_used_at(multi-use — see O2), and returns{agent_id, token}exactly as today. - The agent list on the same page shows the new agent's
name,hardware[],last_heartbeat, and Revoke / Rename actions.
Scope split
Toolkit (ria-toolkit-oss)
The CLI already sends X-API-Key, so no protocol change is required. Two
small quality-of-life changes:
| # | Change | File |
|---|---|---|
| T1 | Update --api-key help text and cli.py:8 docstring to say "personal registration key from the RIA Agents page" rather than "Hub API key". |
agent/cli.py |
| T2 | On registration failure, if the response body is JSON with a reason field (invalid_key / expired / already_consumed / revoked), surface it verbatim instead of the raw HTTPError. Makes user-facing errors actionable. |
agent/cli.py:56-61 |
No change to config.py, ws_client.py, or the streamer — the token
returned by register is still what authenticates the WS connection.
Hub (ria-hub / controller)
Paths below are inferred from screens_agent_handoff.md
(controller/app/modules/...). Hub team should sanity-check before starting.
Prior art — check RIA Conductor first
The RIA Conductor feature is believed to already implement similar key generation (likely for authenticating conductors to the hub). Before building anything in this section, read the Conductor key code and decide whether to:
- Reuse it as-is (shared
registration_keystable,kindcolumn discriminatingconductorvs.agent) — preferred if the shapes line up. - Extract the hashing / minting / revoke primitives into a shared
registration_keysmodule that both features depend on. - Fork a parallel
agent_registration_keystable — only if the Conductor model is materially different (e.g. per-org scoping, different lifetime rules) and forcing a merge would distort one or both features.
Whichever path is chosen should be decided up front and noted on the PR, so we don't end up with two near-identical key subsystems by accident. The security notes below (argon2id, one-time reveal, rate limits, audit logging) apply regardless of which path is taken — confirm Conductor already does these; if not, the fix belongs in the shared code, not this feature.
Data model
New collection (Mongo) or table (if Postgres is used for users):
registration_keys
_id
user_id # FK to hub users
name # user-supplied label, e.g. "lab laptop"
key_prefix # first 8 chars of the plaintext, for UI display
key_hash # argon2id or bcrypt of the full plaintext
created_at
expires_at # optional; null = no expiry
consumed_at # null until first successful registration (if one-shot)
revoked_at # null unless explicitly revoked
last_used_at # updated on every successful use (if multi-use)
Augment the existing agents collection with owner_user_id (FK) and
registered_via_key_id (FK to registration_keys._id).
Decide O2 before building: one-shot vs. reusable. Recommendation: one-shot by default with an optional "reusable for N days" toggle, since one-shot is the lower-blast-radius default and matches how GitHub/Gitea deploy keys behave.
Endpoints
| # | Endpoint | Notes |
|---|---|---|
| H1 | POST /api/v1/user/registration-keys |
Auth: session cookie. Body: {name, expires_in_days?, reusable?}. Returns plaintext key once. |
| H2 | GET /api/v1/user/registration-keys |
Auth: session cookie. Lists the caller's keys (prefix + metadata, never plaintext). |
| H3 | DELETE /api/v1/user/registration-keys/{id} |
Auth: session cookie. Revokes. |
| H4 | POST /screens/agents/register (existing) |
Change auth: look up X-API-Key by hash instead of string-compare against [wac] API_KEY. Reject if revoked / expired / consumed. Set owner_user_id on the new agent row. |
| H5 | GET /api/v1/user/agents |
Auth: session cookie. Lists the caller's agents for the UI. |
| H6 | DELETE /api/v1/user/agents/{id} |
Auth: session cookie. De-registers and closes any live WS. |
H4 is the only backwards-incompatible change. See the migration section for how to ship it without breaking existing deployments.
Frontend
New page — Settings → RIA Agents — two panels:
- Registration keys: table (name, prefix, created, expires, last used, revoke button) + "Generate" button that opens the one-time-reveal modal.
- Agents: table (name, hardware, status, last heartbeat, rename, revoke).
Matches the existing Gitea-style Settings sidebar if RIA Hub is Gitea-based (O3).
Migration from the shared [wac] API_KEY
The shared key is likely in use on every existing deployment. To avoid a flag day:
- Dual-accept window. H4 accepts either a per-user key (lookup by
hash) or the legacy
[wac] API_KEYstring. When the legacy key is used, the resulting agent hasowner_user_id = nulland a warning is logged. - Admin UI surfaces "unowned" agents so an admin can re-assign them or ask owners to re-register.
- Deprecation window of one release, then H4 rejects the legacy key and
the
[wac] API_KEYconfig is removed fromapp.ini.
No toolkit-side migration needed — existing ~/.ria/agent.json files already
store the post-registration token, which keeps working regardless of how
registration itself was authenticated.
Security notes
- Store
key_hashwith a password hash (argon2id), not a fast hash. The key is a secret-equivalent: treat it like a password. - Plaintext key format:
ria_reg_<base64url of 32 random bytes>. Prefix makes the purpose obvious in leaked logs and lets scanners (trufflehog etc.) recognize it. - One-time reveal in the UI — never persist or re-display the plaintext.
- Rate-limit H4 per source IP and per
key_prefixto blunt brute-force on leaked prefixes. Lock a key out after N failed attempts in M minutes. - Log every H4 call (success + failure, with key prefix and source IP) to the audit trail.
Open questions
- O1. Where does the page live? A top-level
/agentsroute is discoverable;/user/settings/agentsmatches Gitea's existing IA. Pick before F7 (frontend task). - O2. One-shot vs. reusable keys (default and whether both are offered). Recommendation above; needs product sign-off.
- O3. Is RIA Hub's web UI really a Gitea fork? URL patterns
(
/qoherent/-/packages/...,.gitclones) suggest yes, but the "Settings" integration plan depends on confirming this. If it isn't, F7 is a standalone page instead. - O4. Does the agent bearer
tokenneed per-user scoping too, or is ownership-at-registration enough? Today the token is opaque and not tied to a user in the WS handler. Probably fine to defer until after per-user keys ship. - O5. Should admins be able to mint keys on behalf of other users (for onboarding)? If yes, H1 needs an admin-scoped variant.
- O6. Conductor reuse decision — reuse / extract / fork. Must be answered before any hub-side code lands. See "Prior art" above.
Out of scope
- SSO / OIDC for agent-to-hub auth (current
tokenbearer is kept as-is). - Per-agent capability scoping beyond what
--allow-txalready does at registration time. - Fleet provisioning (N agents from one key); covered instead by "reusable" flag in O2 if that's the chosen default.
MVP cut
If the hub team wants the smallest shippable slice:
- H1, H2, H3, H4 (with dual-accept), H5.
- Frontend: registration-keys panel only; reuse the existing agents admin view if one already exists.
- T1 toolkit copy-change.
Defer H6, rename flows, T2, and audit logging to a follow-up.