feat(users): auto-default agent accounts to general-agent role

Previously every account created via POST /users without an explicit
role_id fell through to the `guest` role. Recruitment workflow creates
HF accounts for newly-onboarded agents with --agent-id/--claw-identifier
set, so we can detect "this is an agent" at the backend boundary and pick
a more appropriate default:

  payload.agent_id  set  → general-agent (guest reads + reset-self-apikey)
  payload.agent_id  unset → guest        (human users keep current behavior)

Also adds `general-agent` to init_bootstrap.py's _DEFAULT_ROLES so fresh
deployments seed it on first boot — the role already existed on prod
(created via UI earlier); this is for re-seedability / new envs.

No ClawSkills script changes required: the onboard script already calls
`hf user create --agent-id <id> --claw-identifier <claw>`. The recruitment
workflow.md is updated to note the new default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-24 19:38:06 +01:00
parent 54feb9686c
commit 595391b41b
2 changed files with 37 additions and 4 deletions

View File

@@ -68,11 +68,29 @@ def require_account_creator(
raise HTTPException(status_code=403, detail="Account creation permission required")
def _resolve_user_role(db: Session, role_id: int | None) -> Role:
def _resolve_user_role(db: Session, role_id: int | None, *, is_agent: bool = False) -> Role:
"""Resolve target role for user creation.
Default policy when caller didn't pin role_id:
- is_agent (i.e. payload had agent_id/claw_identifier) → general-agent
- human user → guest
general-agent ≈ guest + user.reset-self-apikey so agents can rotate
their own API key without admin intervention. Created in
init_bootstrap.py on every startup; falls back to guest if absent
(e.g. very old DB that hasn't been re-seeded yet).
"""
if role_id is None:
role = db.query(Role).filter(Role.name == "guest").first()
default_name = "general-agent" if is_agent else "guest"
role = db.query(Role).filter(Role.name == default_name).first()
if not role and is_agent:
# general-agent missing from this DB → fall back to guest, log warn
role = db.query(Role).filter(Role.name == "guest").first()
if not role:
raise HTTPException(status_code=500, detail="Default guest role is missing")
raise HTTPException(
status_code=500,
detail=f"Default role '{default_name}' is missing (DB not seeded)",
)
return role
role = db.query(Role).filter(Role.id == role_id).first()
@@ -112,7 +130,7 @@ def create_user(
if existing_agent:
raise HTTPException(status_code=400, detail="agent_id already in use")
assigned_role = _resolve_user_role(db, user.role_id)
assigned_role = _resolve_user_role(db, user.role_id, is_agent=has_agent_id)
# In OIDC-only mode, ignore any supplied password: the user is created
# passwordless (cannot password-login) and is expected to sign in via a
# bound OIDC identity. API keys still work for such users.