feat(security): external-agent consent loop backend (Trust & Comms Phase 1) - #719
Conversation
Add the request→notify→accept→mint→grant backend for external agents
to self-onboard via a human-approved handshake rather than a config file.
New modules:
- tinyagentos/auth_requests_store.py: AuthRequestsStore (aiosqlite) with
create / get / list_pending / count_pending_for and atomic set_decision
(conditional UPDATE WHERE status='pending' — returns None on conflict)
- tinyagentos/agent_grants_store.py: AgentGrantsStore with add_grant /
list_grants / list_active_grants; tier='once' for Phase 1
- tinyagentos/routes/agent_auth_requests.py: five endpoints —
POST /api/agents/auth-requests (exempt, no auth)
GET /api/agents/auth-requests/{id} (exempt, opaque-id cap)
POST /api/agents/auth-requests/{id}/approve (admin only)
POST /api/agents/auth-requests/{id}/deny (admin only)
GET /api/agents/auth-requests (admin only, list pending)
Wiring:
- app.py: AuthRequestsStore + AgentGrantsStore created, init'd in lifespan,
closed on shutdown, and set eagerly on app.state
- routes/__init__.py: router registered before /api/agents/{name} to avoid
path-param capture
- auth_middleware.py: method-sensitive _is_exempt() replaces the simple
path-in-set check — exempts POST /api/agents/auth-requests (create) and
GET /api/agents/auth-requests/{id} (status poll) while keeping approve /
deny / list admin-gated; token only returned on status==accepted
Approve flow: registry.register(origin='external-selfjoin') → mint_registry_token
→ AgentGrantsStore.add_grant per scope + RelationshipManager.set_permission edge
→ AuthRequestsStore.set_decision(accepted, canonical_id, token, granted_scopes)
Tests (tests/test_auth_requests.py): 20 cases covering store atomicity,
route auth gates, approve/deny/409/429 paths, token visibility, and grants.
Signed-off-by: jaylfc <jaylfc25@gmail.com>
External-selfjoin agents are born 'pending' (governance lifecycle); the consent-loop approve now transitions the minted agent to 'active' after register+grant, so an approved agent is not left in the bus inactive/revocation feed (which would make @taOSmd's gate reject it). Adds a regression assertion. Signed-off-by: jaylfc <jaylfc25@gmail.com>
📝 WalkthroughWalkthroughImplements a Phase‑1 external-agent consent workflow: adds persistent AuthRequestsStore and AgentGrantsStore, wires them into app state, adds path-aware auth exemptions, registers a consent router, implements create/poll/approve/deny/list endpoints with per-request locks and atomic decisions, and adds end-to-end async tests. ChangesExternal Agent Consent Loop
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
tinyagentos/auth_requests_store.py (3)
25-43: 💤 Low valueConsider adding indexes for query performance.
The
list_pendingmethod queriesWHERE status='pending' ORDER BY created_ts, andcount_pending_forqueriesWHERE identity_claim=? AND framework=? AND status='pending'. For deployments with many pending requests, these queries could benefit from composite indexes:CREATE INDEX IF NOT EXISTS idx_auth_requests_status_created ON auth_requests(status, created_ts); CREATE INDEX IF NOT EXISTS idx_auth_requests_identity_framework_status ON auth_requests(identity_claim, framework, status);For Phase 1 with small tables this is not critical, but worth considering for scale.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tinyagentos/auth_requests_store.py` around lines 25 - 43, The schema lacks indexes for queries used by list_pending and count_pending_for which can slow queries at scale; update SCHEMA in auth_requests_store.py to include the two recommended composite indexes (one on status, created_ts and one on identity_claim, framework, status) so the DB creates them if missing, ensuring they align with the WHERE/ORDER BY patterns used by list_pending and count_pending_for; add the CREATE INDEX IF NOT EXISTS statements immediately after the CREATE TABLE statement or in the DB initialization routine that uses SCHEMA to guarantee the indexes exist at startup.
76-114: ⚡ Quick winRemove type ignore and handle potential None from get().
Line 114 suppresses a type error:
get()returnsOptional[dict]butcreate()is declared to returndict. Ifget()returnsNoneafter insertion (e.g., due to disk full, database corruption), this would break the contract.🛡️ Proposed fix
- return await self.get(request_id) # type: ignore[return-value] + record = await self.get(request_id) + if record is None: + raise RuntimeError(f"Failed to retrieve auth request {request_id} after insert") + return record🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tinyagentos/auth_requests_store.py` around lines 76 - 114, create() currently returns await self.get(request_id) with a type ignore while get() is Optional[dict]; instead call self.get(request_id) into a variable (e.g., record = await self.get(request_id)), check if record is None and raise an explicit RuntimeError (including request_id/context) if so, otherwise return record so the function's dict return type is satisfied and the type ignore can be removed; update any relevant docstring/comments in create() to reflect the explicit error path.
48-59: 💤 Low valueConsider logging JSON parsing errors.
Lines 54-56 catch broad exceptions (
ValueError,TypeError) and silently fall back to[]. If the database contains corrupted JSON, this would hide the issue. Consider logging the error or being more selective about which exceptions to catch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tinyagentos/auth_requests_store.py` around lines 48 - 59, In _row_to_dict, replace the broad except handling for json parsing with catching json.JSONDecodeError (and TypeError if needed) and log the parsing failure so corrupted DB values aren’t silently ignored; use the module logger (e.g., logging.getLogger(__name__)) to record the exception, include the field name (requested_scopes/requested_skills/granted_scopes), the raw value, and any identifying key from the row (e.g., row.get("id")) in the log, then fall back to [] (or None for granted_scopes) as currently implemented.tinyagentos/agent_grants_store.py (1)
22-33: 💤 Low valueConsider adding indexes for grant queries.
list_grantsqueries bycanonical_idandlist_active_grantsfilters byexpires_at. For larger deployments, consider:CREATE INDEX IF NOT EXISTS idx_agent_grants_canonical_id ON agent_grants(canonical_id); CREATE INDEX IF NOT EXISTS idx_agent_grants_expires_at ON agent_grants(expires_at);Phase 1 tables will be small, so this is a future optimization.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tinyagentos/agent_grants_store.py` around lines 22 - 33, Add indexes to the agent_grants schema to speed queries by canonical_id and expires_at: update the SCHEMA string (where agent_grants table is defined) to include "CREATE INDEX IF NOT EXISTS idx_agent_grants_canonical_id ON agent_grants(canonical_id);" and "CREATE INDEX IF NOT EXISTS idx_agent_grants_expires_at ON agent_grants(expires_at);", so list_grants (which queries by canonical_id) and list_active_grants (which filters by expires_at) will use the indexes for larger datasets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tinyagentos/routes/agent_auth_requests.py`:
- Around line 192-243: The race creates orphaned registry/grant/relationship
records because side-effects (registry.register, mint_registry_token,
grants_store.add_grant, rel_mgr.set_permission) run before the atomic
auth_store.set_decision; fix by ensuring only the winner performs side-effects:
acquire a mutual exclusion keyed on request_id (e.g., advisory DB lock or
in-memory lock) at the start of the approve path and hold it until after
set_decision and side-effects complete, OR flip the flow so you call
auth_store.set_decision(...) first to claim the request (transition to
"accepted" or a provisional state) and only if that succeeds perform
registry.register, mint_registry_token, grants_store.add_grant and
rel_mgr.set_permission, and finally update the decision with canonical_id/token
if needed; if you choose to keep current ordering, add compensating cleanup in
the 409 branch to remove the created registry entry, grants, and relationship
edges (use registry.unregister/delete, grants_store.remove_grant,
rel_mgr.delete_permission) to avoid orphans.
- Around line 285-302: The endpoint list_auth_requests accepts a status query
but ignores it; add validation at the start of list_auth_requests to only allow
"pending" (Phase 1) and reject any other value by raising an HTTPException (400
Bad Request) with a clear message that only status=pending is supported, so
callers passing "all" or "accepted" are not silently ignored; keep the existing
call to store.list_pending() when status=="pending".
---
Nitpick comments:
In `@tinyagentos/agent_grants_store.py`:
- Around line 22-33: Add indexes to the agent_grants schema to speed queries by
canonical_id and expires_at: update the SCHEMA string (where agent_grants table
is defined) to include "CREATE INDEX IF NOT EXISTS idx_agent_grants_canonical_id
ON agent_grants(canonical_id);" and "CREATE INDEX IF NOT EXISTS
idx_agent_grants_expires_at ON agent_grants(expires_at);", so list_grants (which
queries by canonical_id) and list_active_grants (which filters by expires_at)
will use the indexes for larger datasets.
In `@tinyagentos/auth_requests_store.py`:
- Around line 25-43: The schema lacks indexes for queries used by list_pending
and count_pending_for which can slow queries at scale; update SCHEMA in
auth_requests_store.py to include the two recommended composite indexes (one on
status, created_ts and one on identity_claim, framework, status) so the DB
creates them if missing, ensuring they align with the WHERE/ORDER BY patterns
used by list_pending and count_pending_for; add the CREATE INDEX IF NOT EXISTS
statements immediately after the CREATE TABLE statement or in the DB
initialization routine that uses SCHEMA to guarantee the indexes exist at
startup.
- Around line 76-114: create() currently returns await self.get(request_id) with
a type ignore while get() is Optional[dict]; instead call self.get(request_id)
into a variable (e.g., record = await self.get(request_id)), check if record is
None and raise an explicit RuntimeError (including request_id/context) if so,
otherwise return record so the function's dict return type is satisfied and the
type ignore can be removed; update any relevant docstring/comments in create()
to reflect the explicit error path.
- Around line 48-59: In _row_to_dict, replace the broad except handling for json
parsing with catching json.JSONDecodeError (and TypeError if needed) and log the
parsing failure so corrupted DB values aren’t silently ignored; use the module
logger (e.g., logging.getLogger(__name__)) to record the exception, include the
field name (requested_scopes/requested_skills/granted_scopes), the raw value,
and any identifying key from the row (e.g., row.get("id")) in the log, then fall
back to [] (or None for granted_scopes) as currently implemented.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 116f3ccb-348f-482d-8e7c-1fcd83fc112a
📒 Files selected for processing (7)
tests/test_auth_requests.pytinyagentos/agent_grants_store.pytinyagentos/app.pytinyagentos/auth_middleware.pytinyagentos/auth_requests_store.pytinyagentos/routes/__init__.pytinyagentos/routes/agent_auth_requests.py
…ted ?status Two concurrent approvals of the same auth-request could both pass the pending-status check, each register a separate registry entry and write duplicate grants, then one would win set_decision and the other return 409 with orphaned side-effects. Fix: serialise approve calls per request_id with an asyncio.Lock stored on app.state._approve_locks. Also: list endpoint now returns 400 for any status value other than 'pending' (Phase 1 only supports the pending feed), and removes the misleading 'pass status=all' hint from the docstring. auth_requests_store.py: remove # type: ignore on create(); raise RuntimeError if get() returns None immediately after insert (invariant violation rather than silent type lie). Tests: +2 (concurrent approve, unsupported status=400).
…nforcement
Adds the active grant feed @taOSmd polls to enforce bus access. Admin-only,
mirrors the /revoked and /inactive patterns. Optional ?canonical_id= filter
for per-agent queries. Returns {grants: [{canonical_id, scope, tier,
project_id, granted_at, expires_at}]}. Phase 1: all grants are non-expiring
so the full list is always active.
Tests: +3 (feed populated after approve, 403 for non-admin, canonical_id filter).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tinyagentos/routes/agent_registry.py`:
- Around line 227-230: The route currently calls
grants_store.list_grants(canonical_id) which returns all grants for a
canonical_id without filtering expired entries; change the code so the
canonical_id path only returns active grants by either (A) calling
grants_store.list_active_grants() and then filtering by canonical_id (preserving
ORDER BY granted_at), or (B) updating AgentGrantsStore.list_grants(canonical_id)
to include the same expiry predicate (expires_at IS NULL OR expires_at > now())
in its query; ensure the chosen approach applies the active-filter and keeps the
granted_at ordering for GET /api/agents/registry/grants.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ea3603be-cfcd-456b-9498-4f39dee67273
📒 Files selected for processing (2)
tests/test_auth_requests.pytinyagentos/routes/agent_registry.py
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/test_auth_requests.py
| if canonical_id: | ||
| grants = await grants_store.list_grants(canonical_id) | ||
| else: | ||
| grants = await grants_store.list_active_grants() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if list_grants method filters by expiry like list_active_grants does
# Search for the list_grants method implementation in AgentGrantsStore
ast-grep --pattern $'class AgentGrantsStore {
$$$
async def list_grants($$$) {
$$$
}
$$$
}'
# Also search with ripgrep to find the method and see surrounding context
rg -n -A 20 'async def list_grants' --type pyRepository: jaylfc/taOS
Length of output: 1794
Fix canonical_id grants path to apply “active” (expires_at) filtering
GET /api/agents/registry/grants is documented as an “active grant feed” (active iff expires_at IS NULL OR expires_at > now), but when ?canonical_id= is provided it calls AgentGrantsStore.list_grants(canonical_id). That method selects grants by canonical_id only (SELECT * FROM agent_grants WHERE canonical_id = ? ORDER BY granted_at) and has no expires_at predicate, so expired grants can be returned on the canonical-id path. Update the route/store to use expiry-filtered results for the canonical_id path (e.g., reuse list_active_grants() and filter by canonical_id, or add the same expires_at condition to list_grants).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tinyagentos/routes/agent_registry.py` around lines 227 - 230, The route
currently calls grants_store.list_grants(canonical_id) which returns all grants
for a canonical_id without filtering expired entries; change the code so the
canonical_id path only returns active grants by either (A) calling
grants_store.list_active_grants() and then filtering by canonical_id (preserving
ORDER BY granted_at), or (B) updating AgentGrantsStore.list_grants(canonical_id)
to include the same expiry predicate (expires_at IS NULL OR expires_at > now())
in its query; ensure the chosen approach applies the active-filter and keeps the
granted_at ordering for GET /api/agents/registry/grants.
Phase 1 of the Trust & Comms Layer (spec:
docs/superpowers/specs/2026-06-09-trust-comms-layer-design.md) — the external-agent consent loop backend.What
auth_requests_store.py— pending/accepted/refused requests; atomicset_decision(UPDATE…WHERE status='pending').agent_grants_store.py— minimal per-scope grants (canonical_id, scope, tier, expires_at);list_active_grants()for the feed @taOSmd polls.routes/agent_auth_requests.py—POST /api/agents/auth-requests(public — external agent's front door) +GET .../{id}status poll (public; token returned only on accepted) +POST .../{id}/approve|deny+GETlist (admin-only). Approve flow: registry.register (mint canonical_id) → set_status active → mint EdDSA-JWT → grants → atomic accept.auth_middleware.py— method-sensitive_is_exempt: exempts onlyPOSTcreate + single-segmentGETstatus; approve/deny/list require admin auth.Contract (confirmed w/ @taOSmd)
Token = identity-only (sub=canonical_id, user_id, iss=taos-registry); all tiering in the grant store/feed; bus gate = valid token AND active grant.
Security review notes
/in tail; list has no trailing slash).acceptedstatus; request_id is an opaque uuid capability.65 tests pass (20 new + registry + auth_context). Desktop notification UI (per-scope toggles, impeccable skill) is the next build.
Summary by CodeRabbit
New Features
Tests