Three coupled fixes so non-admin agents (e.g. nav, role=mgr) can
actually create projects through hf-cli with their API key:
1. POST /projects no longer hardcodes is_admin. It checks the global
`project.create` perm via role_permissions (admin still wins via
is_admin short-circuit). Permission-denied 403 message names the
exact perm.
2. /auth/me/permissions now uses get_current_user_or_apikey (was
get_current_user JWT-only). This is what hf-cli hits to populate
its local permission cache that drives the "not permitted" gate;
previously every API-key-authed agent saw all commands as gated.
3. get_current_user_or_apikey now also accepts an API key delivered
via Authorization: Bearer (in addition to X-API-Key). hf-cli only
knows Bearer; trying to JWT-decode an API key string would fail —
so on decode failure, fall through to the API key lookup. Keeps
X-API-Key behavior unchanged.
4. init_bootstrap: add `project.create` to DEFAULT_PERMISSIONS and to
_MGR_PERMISSIONS so admin (auto-all) + mgr both get it on seed.
Bug came to light when manager-agent reported `hf project list`/`create`
returned `not permitted`. Root cause: hf-cli calls /auth/me/permissions
with the API key via Bearer header → 401 → state.Known=false → every
command in the surface is gated false locally. Even after the local
gate, POST /projects would still 403 due to the hardcoded admin check.
All four steps above are required end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit cross-referenced every check_permission / _has_permission /
_has_global_permission / _require_calendar_permission call against
init_bootstrap.DEFAULT_PERMISSIONS. Three were enforced in code but
never seeded, so the Role Editor couldn't expose them:
- member.remove (projects.py:357 — remove project member)
- schedule_type.read (schedule_type.py + schedule_type_special_slot.py)
- schedule_type.manage (schedule_type.py + schedule_type_special_slot.py)
Seed only — default roles are NOT modified (admin still gets everything
via the "None = all perms" rule). Operators can grant via Role Editor.
Other audit notes (not fixed in this commit, separate decisions):
- GET /projects + GET /projects/{id}/members are completely unauthed
(no Depends(get_current_user_or_apikey)). Anonymous can list all
projects. Investigate whether this is deliberate (e.g. for monitor
external scrape) or an oversight.
- create_project hardcodes `if not current_user.is_admin: 403 "Only
admins can create projects"` — doesn't consult permissions at all.
Means manager-role users can't create projects even if they have
project.write or hypothetical project.create. Consider switching
to a perm-based gate.
- Several catalog perms (project.*, task.create/read/write/delete,
milestone.*) are seeded but never checked in code; basic CRUD on
task/project/milestone/comment is gated via the parallel
check_project_role (viewer/member/dev/mgr ladder) instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>