Gap (mwiget, non-blocking note on #186)
backend/routes/benchmarks.py::_require_agent_bearer authorizes the agent-facing mutating endpoints (register / ingest) on the role claim alone, without loading a User — the same fail-open shape #186 fixed for the WebSocket validators. A deactivated or must-change-password operator/admin whose JWT is still valid can keep writing to these endpoints until the token expires.
Why it is not a drop-in "same fix"
Validated the code before acting: _AGENT_WRITE_ROLES = {agent, operator, admin}, and agent tokens intentionally have no User row — their sub is an agent name or a service identity, not a username:
startup_steps.py: {"sub": "forge-builtin-agent", "role": "agent"}
benchmark_agent_provision_service.py: {"sub": agent.name, "role": "agent", "agent_id": ...}
- tests also cover
{"sub": "forge-agent-service", "role": "admin"} and role=admin+agent_id service tokens.
So naively calling token_user_state(token) here (as the WS gate does) would reject every legitimate agent/service token and break benchmark ingestion. This is why it was left out of #186 rather than rushed into an approved PR.
Proposed fix (own PR, with tests)
Carve out by role:
role == "agent" (and service tokens without a username sub): keep the claim-only check — there is no User to load.
role in {"operator", "admin"} with a username sub: resolve via token_user_state and refuse on None / is_active == False / must_change_password, matching get_current_user.
Extend backend/tests/unit/test_benchmark_agent_auth.py accordingly (deactivated operator refused; agent/service tokens still accepted). Reference: token_user_state in services/auth_service.py.
https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Gap (mwiget, non-blocking note on #186)
backend/routes/benchmarks.py::_require_agent_bearerauthorizes the agent-facing mutating endpoints (register / ingest) on the role claim alone, without loading aUser— the same fail-open shape #186 fixed for the WebSocket validators. A deactivated or must-change-password operator/admin whose JWT is still valid can keep writing to these endpoints until the token expires.Why it is not a drop-in "same fix"
Validated the code before acting:
_AGENT_WRITE_ROLES = {agent, operator, admin}, and agent tokens intentionally have noUserrow — theirsubis an agent name or a service identity, not a username:startup_steps.py:{"sub": "forge-builtin-agent", "role": "agent"}benchmark_agent_provision_service.py:{"sub": agent.name, "role": "agent", "agent_id": ...}{"sub": "forge-agent-service", "role": "admin"}androle=admin+agent_idservice tokens.So naively calling
token_user_state(token)here (as the WS gate does) would reject every legitimate agent/service token and break benchmark ingestion. This is why it was left out of #186 rather than rushed into an approved PR.Proposed fix (own PR, with tests)
Carve out by role:
role == "agent"(and service tokens without a usernamesub): keep the claim-only check — there is noUserto load.role in {"operator", "admin"}with a usernamesub: resolve viatoken_user_stateand refuse onNone/is_active == False/must_change_password, matchingget_current_user.Extend
backend/tests/unit/test_benchmark_agent_auth.pyaccordingly (deactivated operator refused; agent/service tokens still accepted). Reference:token_user_stateinservices/auth_service.py.https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4