Found while onboarding the taOSmobile agent on 2026-07-21. Jay approved an invite granting eight scopes including project_tasks, the grants are recorded correctly, and the agent still cannot create tasks.
The gap
create_task in tinyagentos/routes/projects.py:587 authorises via _get_owned_project, which is:
if not user.is_admin and user.user_id != p["user_id"]:
return JSONResponse({"error": "not found"}, status_code=404)
Owner or admin only. The grants table is never consulted. So an agent holding an active project_tasks grant on that exact project gets a masked 404, identical to what a stranger sees.
Verified on the live instance: GET /api/agents/registry/grants?canonical_id=... returns all eight scopes as active, project-scoped to prj-qlvxdq:
a2a_send, a2a_receive, project_tasks, canvas_read,
canvas_write, files_read, files_write, memory_read (tier=once, project=prj-qlvxdq)
tier='once' is a misleading label but not the problem: the store docstring defines it as "single-decision, no expiry", so these are permanent.
Why it matters
The consent flow, the invite scopes, the approval prompt and the grant feed all work. The enforcement point does not read any of it, so approving project_tasks for an external agent currently grants nothing. That is worse than refusing the scope, because the UI reports success and the agent is left guessing why every call 404s.
It also means the scope set offered at invite time is partly fictional. Worth auditing which other scopes have a real enforcement point and which are similarly decorative.
Fix
Authorise project routes against owner, admin, or an active grant for the required scope on that project. Roughly:
- Add a helper alongside
_get_owned_project, e.g. _get_project_for_scope(pstore, grants, project_id, user, scope).
- Keep the masked 404 for genuine strangers so existence is still hidden.
- Apply it to the task routes first (
create_task, claim, release, close, comment), since project_tasks is the scope we actually hand out.
- Add a negative test per route: an agent WITHOUT the grant still gets 404, an agent WITH it succeeds. Without both halves the test proves nothing.
Audit needed
Check every scope offered by the invite dialog against a real enforcement point:
project_tasks, canvas_read, canvas_write, files_read, files_write, memory_read, memory_write, decisions_read, decisions_write, registry_feeds_read, tools_execute, a2a_send, a2a_receive.
a2a_send and a2a_receive are known to be enforced (the bus reads the grant feed). The rest need confirming rather than assuming.
Related: #1920 and PR #1921 (agent requests additional scopes, which is the tier above this and pointless until this is fixed), #133 (per-agent permission model), #552 (multi-user isolation).
Found while onboarding the taOSmobile agent on 2026-07-21. Jay approved an invite granting eight scopes including
project_tasks, the grants are recorded correctly, and the agent still cannot create tasks.The gap
create_taskintinyagentos/routes/projects.py:587authorises via_get_owned_project, which is:Owner or admin only. The grants table is never consulted. So an agent holding an active
project_tasksgrant on that exact project gets a masked 404, identical to what a stranger sees.Verified on the live instance:
GET /api/agents/registry/grants?canonical_id=...returns all eight scopes as active, project-scoped toprj-qlvxdq:tier='once'is a misleading label but not the problem: the store docstring defines it as "single-decision, no expiry", so these are permanent.Why it matters
The consent flow, the invite scopes, the approval prompt and the grant feed all work. The enforcement point does not read any of it, so approving
project_tasksfor an external agent currently grants nothing. That is worse than refusing the scope, because the UI reports success and the agent is left guessing why every call 404s.It also means the scope set offered at invite time is partly fictional. Worth auditing which other scopes have a real enforcement point and which are similarly decorative.
Fix
Authorise project routes against owner, admin, or an active grant for the required scope on that project. Roughly:
_get_owned_project, e.g._get_project_for_scope(pstore, grants, project_id, user, scope).create_task, claim, release, close, comment), sinceproject_tasksis the scope we actually hand out.Audit needed
Check every scope offered by the invite dialog against a real enforcement point:
project_tasks,canvas_read,canvas_write,files_read,files_write,memory_read,memory_write,decisions_read,decisions_write,registry_feeds_read,tools_execute,a2a_send,a2a_receive.a2a_sendanda2a_receiveare known to be enforced (the bus reads the grant feed). The rest need confirming rather than assuming.Related: #1920 and PR #1921 (agent requests additional scopes, which is the tier above this and pointless until this is fixed), #133 (per-agent permission model), #552 (multi-user isolation).