SECURITY: agent scope grants can be created but never revoked
Found 2026-07-26 while trying to honour a routine revocation request: @taOS-website-dev
finished a cross-project review, asked for their files_read grant on the taOSc project
to be removed, and there is no way to do it.
The gap
tinyagentos/agent_grants_store.py exposes exactly three operations:
add_grant(...)
list_grants(canonical_id)
list_active_grants()
There is no revoke, no delete, no expire, at the store layer or as a route. Once a
scope is granted to an agent identity through the scope-request flow, it is permanent for
the life of that identity.
Consequences:
- Least privilege is unachievable. Grants only accumulate. An agent that needed read
access to one project for one review keeps it forever.
- The only removal is nuclear.
agent_registry_store.revoke(canonical_id) kills the
whole identity - every scope on every project - and forces a re-mint. That is not a
proportionate response to "this review is finished".
expires_at exists in the schema and nothing ever sets it. Time-boxed grants are
half-built: the column is there, add_grant always leaves it null.
Worse: a path that LOOKS like revocation and is not
POST /api/projects/{id}/members/assign-agent with "scopes": [] returns
{"granted_scopes": []} - a success response that reads exactly like a revocation.
It is not. Verified today: after that call the registry grant (id 199, files_read,
prj-b6ao4t) was still present and still active. Project member scopes and registry
grants are two different stores, and the admin surface silently reconciles only one.
An operator following the obvious path believes they removed access. They did not.
That is worse than having no revoke path at all.
What is needed
revoke_grant(canonical_id, scope, project_id) on the grants store, plus
revoke_all_for_project(canonical_id, project_id) for the common case.
- An owner/admin route to call it. Deny by default, audit-logged like the approve path.
- Make
assign-agent honest: reducing or clearing scopes must reconcile the registry
grants too, or must fail loudly rather than report a success it did not perform.
- Populate
expires_at and honour it at check time, so a grant can be time-boxed to the
job it was requested for.
- Surface active grants per agent in the Agents app with a Revoke action - an operator
should not need the API to undo a grant they made in the UI.
Acceptance
- A granted scope can be revoked individually, and the agent's next request for that
scope is refused.
- Revoking one scope on one project leaves the identity's other scopes and projects intact.
assign-agent with reduced scopes either reconciles registry grants or errors; a test
asserts it can no longer report success while access persists.
- An expired grant is refused at check time.
- Revocation and expiry are audit-logged.
- Existing grant/approve flows unchanged.
SECURITY: agent scope grants can be created but never revoked
Found 2026-07-26 while trying to honour a routine revocation request: @taOS-website-dev
finished a cross-project review, asked for their
files_readgrant on the taOSc projectto be removed, and there is no way to do it.
The gap
tinyagentos/agent_grants_store.pyexposes exactly three operations:add_grant(...)list_grants(canonical_id)list_active_grants()There is no revoke, no delete, no expire, at the store layer or as a route. Once a
scope is granted to an agent identity through the scope-request flow, it is permanent for
the life of that identity.
Consequences:
access to one project for one review keeps it forever.
agent_registry_store.revoke(canonical_id)kills thewhole identity - every scope on every project - and forces a re-mint. That is not a
proportionate response to "this review is finished".
expires_atexists in the schema and nothing ever sets it. Time-boxed grants arehalf-built: the column is there,
add_grantalways leaves it null.Worse: a path that LOOKS like revocation and is not
POST /api/projects/{id}/members/assign-agentwith"scopes": []returns{"granted_scopes": []}- a success response that reads exactly like a revocation.It is not. Verified today: after that call the registry grant (
id 199,files_read,prj-b6ao4t) was still present and still active. Project member scopes and registrygrants are two different stores, and the admin surface silently reconciles only one.
An operator following the obvious path believes they removed access. They did not.
That is worse than having no revoke path at all.
What is needed
revoke_grant(canonical_id, scope, project_id)on the grants store, plusrevoke_all_for_project(canonical_id, project_id)for the common case.assign-agenthonest: reducing or clearing scopes must reconcile the registrygrants too, or must fail loudly rather than report a success it did not perform.
expires_atand honour it at check time, so a grant can be time-boxed to thejob it was requested for.
should not need the API to undo a grant they made in the UI.
Acceptance
scope is refused.
assign-agentwith reduced scopes either reconciles registry grants or errors; a testasserts it can no longer report success while access persists.