diff --git a/app/api/routers/users.py b/app/api/routers/users.py index fee741c..0dfd908 100644 --- a/app/api/routers/users.py +++ b/app/api/routers/users.py @@ -221,6 +221,71 @@ def update_user( return _user_response(user) +@router.patch("/{identifier}/bind-agent", response_model=schemas.UserResponse) +def bind_agent( + identifier: str, + payload: schemas.UserBindAgentRequest, + db: Session = Depends(get_db), + _: models.User = Depends(require_account_creator), +): + """Bind an existing user to (agent_id, claw_identifier). + + Backfill path for users that were created via `hf user create` before + the cli supported `--agent-id` / `--claw-identifier` flags. Creates + the `agents` row that should have been written at user-create time. + + Idempotent: if the user is already bound to the same + (agent_id, claw_identifier), returns the user unchanged (200, no-op). + + Rejects (409) if: + - the user is bound to a DIFFERENT (agent_id, claw_identifier) + - the requested agent_id is already in use by another user + + Permission: account.create (admin auto-grants) — same gate as + POST /users so the surface stays symmetric. + """ + user = _find_user_by_id_or_username(db, identifier) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + existing_agent_for_user = db.query(Agent).filter(Agent.user_id == user.id).first() + if existing_agent_for_user: + if ( + existing_agent_for_user.agent_id == payload.agent_id + and existing_agent_for_user.claw_identifier == payload.claw_identifier + ): + # idempotent re-bind + return _user_response(user) + raise HTTPException( + status_code=409, + detail=( + f"User '{user.username}' is already bound to agent " + f"'{existing_agent_for_user.agent_id}' on claw " + f"'{existing_agent_for_user.claw_identifier}'" + ), + ) + + existing_for_agent_id = ( + db.query(Agent).filter(Agent.agent_id == payload.agent_id).first() + ) + if existing_for_agent_id: + raise HTTPException( + status_code=409, + detail=f"agent_id '{payload.agent_id}' already in use by another user", + ) + + db.add( + Agent( + user_id=user.id, + agent_id=payload.agent_id, + claw_identifier=payload.claw_identifier, + ) + ) + db.commit() + db.refresh(user) + return _user_response(user) + + _BUILTIN_USERNAMES = {"acc-mgr", DELETED_USER_USERNAME} diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 5f3ebe6..8fc2beb 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import Optional, List from datetime import datetime, time from enum import Enum @@ -186,6 +186,19 @@ class UserUpdate(BaseModel): discord_user_id: Optional[str] = None +class UserBindAgentRequest(BaseModel): + """Request body for PATCH /users/{identifier}/bind-agent. + + Binds an existing user to (agent_id, claw_identifier) by inserting a + row in the `agents` table. Both fields required (mirrors the + create-time invariant in UserCreate). Idempotent: re-binding the same + user to the same (agent_id, claw_identifier) returns the existing + Agent row instead of 409. + """ + agent_id: str = Field(..., min_length=1, max_length=128) + claw_identifier: str = Field(..., min_length=1, max_length=128) + + class UserResponse(UserBase): id: int is_active: bool