Skip to content

feat(auth): per-agent registry-identity auth for the A2A bus read proxy (#138 Phase 1) - #1507

Merged
jaylfc merged 3 commits into
devfrom
feat/agent-identity-auth
Jun 29, 2026
Merged

feat(auth): per-agent registry-identity auth for the A2A bus read proxy (#138 Phase 1)#1507
jaylfc merged 3 commits into
devfrom
feat/agent-identity-auth

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Phase 1 of #138: the driver agents (@taOS-dev, @taOSmd-dev, @taOS-website-dev, @Hermes) authenticate with their OWN taOS agent-registry identity + scoped Ed25519 JWT instead of Jay's owner password.

What this does

  • A registered, active agent reads the A2A bus read-proxy (GET /api/a2a/bus/channels|messages) with Authorization: Bearer <registry-JWT>, authorized by an a2a_receive scope grant. No owner session/password needed.
  • New shared verify helper check_agent_scope(request, scope) (tinyagentos/agent_token_auth.py), fail-closed: 401 bad-sig/malformed/missing-sub, 403 inactive/unknown-sub/missing-or-expired-grant. _check_feed_token now delegates to it (feed routes unchanged).
  • Middleware allowlist is exact (_AGENT_TOKEN_PATHS = feeds | bus-read): a registry JWT can never authenticate any other route (skeleton-key guard, tested). Local-token admin path runs first and is unchanged.
  • Admin-only POST /api/agents/registry/mint-internal + /seed-internal, and taosctl agents mint|seed-internal, to mint the 4 identities (idempotent by handle; get_by_handle added to the registry store).

Reviewer notes

  • BEHAVIOR CHANGE: bus read now requires admin OR an agent with a2a_receive. Previously any authenticated session could read it. Harmless today (single admin); flagging in case non-admin users should retain bus-read (one-line loosen to user_id is not None).
  • Bus-side enforcement (binding from to the verified identity, verify-and-warn -> enforce) is Phase 2 in the taosmd repo; this PR is controller read-auth + identity minting only.
  • No message stores touched. chat.db (912) + the full ~/.taosmd tree were backed up before any work.

Tests

25 new across test_a2a_bus_agent_auth.py (12 edge cases incl. skeleton-key, suspended, revoked, foreign-key, expired-grant, admin/local-token regressions), test_agent_internal_mint.py, test_taosctl_agents_mint.py. Feed-route + existing a2a-bus regressions pass unchanged.

Summary by CodeRabbit

  • New Features
    • Added agent-identity-based authentication using signed registry JWTs for supported read-only APIs.
    • Added admin actions to mint and seed internal agent identities and tokens (idempotent by handle/seed reruns).
    • Expanded CLI support for agents mint and seed-internal.
  • Bug Fixes
    • Enforced fail-closed authorization for protected registry feed and read-only A2A bus routes (active agent + unexpired scope grants).
  • Documentation
    • Updated onboarding and authentication/authorization details in the README and external-agent onboarding guide.
  • Tests
    • Added end-to-end and unit coverage for agent-token scope/expiry handling, mint/seed behavior, and CLI request behavior.

…hase 1)

Agents read the A2A bus with their OWN Ed25519 registry identity (scope
a2a_receive), never the owner session/password.

- agent_token_auth.check_agent_scope: shared verify path (401 malformed/bad-sig,
  403 inactive/missing-scope/expired-grant, None when no Bearer). _check_feed_token
  now delegates to it so the registry feed routes behave identically.
- auth_middleware: extend the non-local-token Bearer passthrough allowlist to the
  two bus-read paths via _AGENT_TOKEN_PATHS (exact match, no skeleton key). The
  admin local-token branch still runs first and keeps admin semantics.
- routes/a2a_bus: gate both reads -- admin session/local-token pass through; any
  other Bearer must hold an active a2a_receive grant.
- agent_registry_store.get_by_handle: idempotency lookup for internal minting.
- routes/agent_registry: admin-only mint-internal + seed-internal endpoints
  (idempotent by handle, ensure grants, mint + return the JWT; never written to disk).
- taosctl agents mint / seed-internal.

Tests cover the full edge matrix (grant present/absent, suspended, revoked,
malformed, foreign-key, unknown sub, expired grant), admin + local-token
regressions, the skeleton-key guard, store get_by_handle, and mint/seed
idempotency. Docs: README auth section + external-agent-onboarding Phase 1 note.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared registry-JWT scope checks, expands agent-token authorization to A2A bus reads, introduces internal agent mint/seed endpoints and matching CLI commands, and updates docs and tests for the new identity flow.

Changes

Agent JWT Auth for A2A Bus + Internal Mint/Seed

Layer / File(s) Summary
Registry lookup and shared scope checks
tinyagentos/agent_registry_store.py, tinyagentos/agent_token_auth.py, tests/test_agent_internal_mint.py, tests/test_a2a_bus_agent_auth.py
Adds handle-based registry lookup and a shared registry-JWT verifier that checks token signatures, active agent status, and unexpired grants. Tests cover handle lookup and grant-expiry handling.
A2A bus allowlisting and route authorization
tinyagentos/auth_middleware.py, tinyagentos/routes/a2a_bus.py, tests/test_a2a_bus_agent_auth.py
Expands Bearer-token allowlisting to the A2A bus read routes and authorizes bus reads with a2a_receive scope. Tests cover allowed reads, denial cases, admin fallback, and the non-allowlisted route guardrail.
Internal agent minting and CLI wiring
tinyagentos/routes/agent_registry.py, tinyagentos/cli/taosctl/commands/agents.py, README.md, docs/design/external-agent-onboarding.md, tests/test_agent_internal_mint.py, tests/test_taosctl_agents_mint.py
Adds internal mint/seed routes, request validation and idempotent grant creation, CLI subcommands for both flows, and onboarding docs for token issuance and storage. Tests cover mint idempotency, validation, seeding, token issuance, and CLI request bodies.

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant AuthMiddleware
  participant A2ABusRoute
  participant check_agent_scope
  participant AgentRegistryStore
  participant AgentGrantsStore

  Agent->>AuthMiddleware: GET /api/a2a/bus/channels (Bearer registry JWT)
  AuthMiddleware->>A2ABusRoute: allowlisted path passes through
  A2ABusRoute->>check_agent_scope: request, "a2a_receive"
  check_agent_scope->>AgentRegistryStore: lookup agent by sub
  AgentRegistryStore-->>check_agent_scope: active record
  check_agent_scope->>AgentGrantsStore: list grants for canonical_id
  AgentGrantsStore-->>check_agent_scope: matching grant
  check_agent_scope-->>A2ABusRoute: canonical_id
  A2ABusRoute-->>Agent: channels JSON
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • jaylfc/taOS#808: Introduced registry-JWT route allowlisting and feed-token verification that this PR extends and refactors.
  • jaylfc/taOS#719: Added auth middleware gating changes on specific endpoint paths, which this PR expands for agent tokens.
  • jaylfc/taOS#705: Established the registry identity and JWT foundation that the new internal mint/seed flow builds on.

Poem

🐇 I minted a key in the moonlit breeze,
and hopped through scopes with elegant ease.
Bus reads open, but only the right ones do,
with grants and handles stitched snug and true.
Fail closed, little code, and keep the carrots in view 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: per-agent registry-identity auth for the A2A bus read proxy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-identity-auth

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

if record is None or record.get("status") != "active":
raise HTTPException(status_code=403, detail="agent is not active in the registry")

# Must hold an active grant for the required scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: expires_at is compared as a plain string (g["expires_at"] > now) against a tz-aware ISO 8601 string. This only yields correct results when every stored expires_at is in UTC and uses the same microsecond + offset format that datetime.now(timezone.utc).isoformat() produces (e.g. 2026-01-01T00:00:00.123456+00:00). If anything ever stores a naive datetime, a different precision, or a non-UTC offset, the lexicographic comparison becomes wrong and an "expired" grant could be treated as live (granting access after revocation), or a live grant as expired. Recommend normalizing both sides (e.g. datetime.fromisoformat(...) and comparing epoch) or storing expires_at as an integer epoch ms.


def _get_grants_store(request: Request):
store = getattr(request.app.state, "agent_grants", None)
if store is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: _get_store / _get_grants_store / _get_keypair raise a bare RuntimeError when the expected attribute is missing on app.state. A misconfigured startup (e.g. someone forgets to wire the new stores) would surface as a 500 with a stack trace instead of a controlled 503. Wrap with HTTPException(status_code=503, detail="agent auth not configured") (or a middleware assertion) so the failure mode is clean. Only directly caused by the helpers introduced in this PR.

caller = await check_agent_scope(request, "a2a_receive")
if caller is None:
raise HTTPException(status_code=403, detail="forbidden")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: When check_agent_scope returns None (no Bearer header) and request.state.is_admin is False, this raises 403 forbidden. REST/Auth semantics prefer 401 Unauthorized with a WWW-Authenticate header for callers with no/invalid credentials, and 403 for callers who authenticated but lack permission. Today this branch only fires for callers with no Bearer AND no admin session, which is "not authenticated", not "authenticated-but-forbidden". Recommend raise HTTPException(status_code=401, detail="authentication required", headers={"WWW-Authenticate": "Bearer"}) here. Existing cookie-session + local-token paths are unaffected because middleware sets is_admin=True first.

).fetchone()
return _row_to_dict(row) if row else None

async def get_by_handle(self, handle: str, *, status: str = "active") -> Optional[dict]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: get_by_handle + the caller in _mint_internal_identity form a non-atomic check-then-register pattern with no UNIQUE index on handle. Two concurrent mint-internal calls for the same handle (e.g. an admin running seed-internal twice in parallel, or two admins minting at once) can both see existing is None, both call register(), and produce two distinct rows with different canonical_ids — defeating the idempotency guarantee that the route's docstring and the README promise. Recommend either wrapping the lookup+register+grant block in an explicit SQLite transaction with INSERT ... ON CONFLICT DO NOTHING semantics, or adding a partial UNIQUE index on handle WHERE status = 'active' so the second mint either fails fast or reuses the row. (The PR description does call this out as "idempotent by handle", so the guarantee matters.)

@@ -68,6 +69,23 @@ class PatchRegistryRequest(BaseModel):
capabilities: Optional[list[str]] = None


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: MintInternalRequest.scopes is list[str] with no allowlist and no normalization. An admin can submit typos like ["a2a_recieve"] or ["a2a-receive"]; add_grant will silently insert grant rows that never match anything, and the route happily returns a JWT that grants nothing. Today this is admin-only, so the blast radius is small, but since _INTERNAL_AGENT_SCOPES shows the intended set is tiny (a2a_send, a2a_receive, registry_feeds_read, ...), consider either (a) a Literal/enum of the known scopes with rejection on unknown, or (b) lowercasing + stripping whitespace + dedup before add_grant. This also makes future "what scopes exist?" audits trivial.

@kilo-code-bot

kilo-code-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/routes/agent_registry.py 141 Module-level _mint_lock only serializes mints within a single uvicorn worker; the comment justifying the removal of the DB unique index silently relies on The controller is a single process, which a future --workers 2 (or gunicorn) deploy would silently violate and re-open the race.
tinyagentos/routes/agent_registry.py 260 The lock only covers get_by_handle + register; the grants loop (285-286) and mint_registry_token (288-294) run outside the lock — safe today only because add_grant is idempotent, but the function docstring advertises the whole mint as one idempotent operation.
Files Reviewed (3 files changed since previous review)
  • tests/test_agent_internal_mint.py - 0 issues (new test_concurrent_mint_same_handle_makes_one_row correctly exercises the new _mint_lock; new test_allowed_scopes_matches_canonical_valid_scopes adds a regression guard for the duplicate-list drift risk flagged in the previous review)
  • tinyagentos/agent_registry_store.py - 0 issues (_migration_v2_unique_active_handle removal cleanly closes the previously-flagged WARNING about the broad except Exception swallowing all CREATE UNIQUE INDEX errors — the migration is gone, so the risk is gone)
  • tinyagentos/routes/agent_registry.py - 2 issues (lock scope and lock placement; see above)

Previously flagged issues — status

File Line Original issue Status
tinyagentos/agent_registry_store.py 125 (old) except Exception too broad in _migration_v2_unique_active_handle RESOLVED — entire migration removed by this PR.
tinyagentos/routes/agent_registry.py 279 (old) Bare raise in except IntegrityError race-loser branch RESOLVED — the try/except IntegrityError block was replaced by the in-process lock; the bare-raise path no longer exists.
tinyagentos/agent_registry_store.py 387 Non-atomic get_by_handle + register RESOLVED — superseded by module-level _mint_lock in agent_registry.py:141 (with the multi-worker caveat noted as a new SUGGESTION).
tinyagentos/routes/agent_registry.py 76 Duplicate _ALLOWED_SCOPES vs VALID_SCOPES STILL OPEN — unchanged code; now guarded by the new test_allowed_scopes_matches_canonical_valid_scopes test, which catches drift but does not eliminate it. Recommend the import-shared-source fix from the previous review.

Fix these issues in Kilo Cloud

Previous Review Summaries (2 snapshots, latest commit 4f9121a)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 4f9121a)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/routes/agent_registry.py 76 _ALLOWED_SCOPES is a hand-maintained duplicate of VALID_SCOPES in agent_auth_requests.py:44; the comment says "Mirrors" — the two lists can drift silently and should share a single source of truth.
tinyagentos/routes/agent_registry.py 279 The except IntegrityError race-loser branch re-raises with a bare raise, surfacing a 500 with a stack trace instead of a clean 409 like the existing-handle path above.

WARNING

File Line Issue
tinyagentos/agent_registry_store.py 125 except Exception swallows every CREATE UNIQUE INDEX error (disk-full, locked DB, permissions, etc.), not just the documented pre-existing-duplicate-handles case — boot continues without the index and the route's IntegrityError handler becomes the sole race-condition barrier.
Files Reviewed (5 files changed since previous review)
  • tests/test_a2a_bus_agent_auth.py - 0 issues (tests for new _grant_unexpired helper cover the main edge cases)
  • tests/test_agent_internal_mint.py - 0 issues (covers the unique-index race and unknown-scope rejection)
  • tinyagentos/agent_registry_store.py - 1 issue (migration v2 broad exception handler)
  • tinyagentos/agent_token_auth.py - 0 issues (_grant_unexpired correctly closes the previous ISO-string comparison warning; the WARNING and SUGGESTION on lines 41/110 are now superseded by the new code on changed lines)
  • tinyagentos/routes/agent_registry.py - 2 issues (duplicate scope allowlist; bare raise in race-loser branch)

Previously flagged issues — status

File Line Original issue Status
tinyagentos/agent_token_auth.py 110 ISO-string expires_at comparison RESOLVED — replaced by _grant_unexpired which parses datetimes; naive timestamps are now assumed UTC and unparseable values fail closed.
tinyagentos/agent_token_auth.py 41 Bare RuntimeError on missing app.state Still present (unchanged code) — not in the incremental diff, so not re-flagged.
tinyagentos/routes/a2a_bus.py 57 403 vs 401 for missing-credentials Still present (unchanged code) — not in the incremental diff, so not re-flagged.
tinyagentos/agent_registry_store.py 413 Non-atomic get_by_handle + register RESOLVED — migration v2 adds the partial UNIQUE INDEX uniq_active_handle, and _mint_internal_identity now catches IntegrityError and re-reads the winner's row (with a guard that the winner is _INTERNAL_ORIGIN).
tinyagentos/routes/agent_registry.py 72 Unvalidated MintInternalRequest.scopes RESOLVED — new _known_scopes field validator rejects unknown scopes against _ALLOWED_SCOPES (issue: allowlist is a duplicate of VALID_SCOPES, see new SUGGESTION above).
tinyagentos/routes/agent_registry.py 285 Reuse-without-origin-check (CodeRabbit) RESOLVED — origin guard added; non-internal existing owner now 409s.

Fix these issues in Kilo Cloud

Previous review (commit a9c2cf4)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/agent_token_auth.py 89 expires_at > now is a lexicographic ISO-string comparison; correct only if every stored expires_at shares datetime.now(timezone.utc).isoformat()'s exact format (UTC + microseconds + offset). Mis-formatted rows let expired grants count as live or vice versa.
tinyagentos/routes/a2a_bus.py 57 Missing-credentials branch raises 403; should be 401 + WWW-Authenticate: Bearer (no Bearer & no admin session = unauthenticated, not unauthorized).
tinyagentos/agent_registry_store.py 386 get_by_handle + _mint_internal_identity are a non-atomic check-then-register with no UNIQUE index on handle; concurrent mints for the same handle can create duplicate rows, breaking the idempotency the route docstring promises.

SUGGESTION

File Line Issue
tinyagentos/agent_token_auth.py 41 _get_store / _get_grants_store / _get_keypair raise bare RuntimeError on missing app.state attrs, producing 500s instead of controlled 503s.
tinyagentos/routes/agent_registry.py 71 MintInternalRequest.scopes is unvalidated free-form list — admin typos silently insert unmatchable grant rows and a JWT that grants nothing.
Files Reviewed (11 files)
  • README.md
  • docs/design/external-agent-onboarding.md
  • tests/test_a2a_bus_agent_auth.py
  • tests/test_agent_internal_mint.py
  • tests/test_taosctl_agents_mint.py
  • tinyagentos/agent_registry_store.py - 1 issue
  • tinyagentos/agent_token_auth.py - 2 issues
  • tinyagentos/auth_middleware.py
  • tinyagentos/cli/taosctl/commands/agents.py
  • tinyagentos/routes/a2a_bus.py - 1 issue
  • tinyagentos/routes/agent_registry.py - 1 issue

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 50.8K · Output: 5.7K · Cached: 536.3K

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tinyagentos/agent_token_auth.py (1)

76-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Suppress exception chaining for the 401 mapping.

Line 77 triggers Ruff B904; use from None so the internal token-parse failure is not chained into the HTTPException.

Proposed fix
-    except ValueError:
-        raise HTTPException(status_code=401, detail="invalid or malformed registry token")
+    except ValueError:
+        raise HTTPException(
+            status_code=401,
+            detail="invalid or malformed registry token",
+        ) from None
🤖 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_token_auth.py` around lines 76 - 77, The
ValueError-to-HTTPException mapping in agent_token_auth should suppress
exception chaining to satisfy Ruff B904. Update the exception handler in the
token parsing flow so the HTTPException raised for the invalid or malformed
registry token uses explicit exception suppression, keeping the internal parse
failure from being chained. Locate the except ValueError block in the token auth
logic and adjust the raise accordingly.

Source: Linters/SAST tools

🤖 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 231-248: Reject any non-internal existing handle before grants are
added in agent_registry logic. In the branch around existing = await
store.get_by_handle(handle), verify the returned record belongs to the internal
origin before reusing it; if it does not, do not proceed to canonical_id or the
grants loop. Update the flow in the register/reuse path so only _INTERNAL_ORIGIN
handles can be reused for scoped grant assignment, and keep the existing
register() path for new internal records.

---

Nitpick comments:
In `@tinyagentos/agent_token_auth.py`:
- Around line 76-77: The ValueError-to-HTTPException mapping in agent_token_auth
should suppress exception chaining to satisfy Ruff B904. Update the exception
handler in the token parsing flow so the HTTPException raised for the invalid or
malformed registry token uses explicit exception suppression, keeping the
internal parse failure from being chained. Locate the except ValueError block in
the token auth logic and adjust the raise accordingly.
🪄 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: c1550c89-735c-49c2-aaec-1f205208ea8f

📥 Commits

Reviewing files that changed from the base of the PR and between c8fd0e7 and a9c2cf4.

📒 Files selected for processing (11)
  • README.md
  • docs/design/external-agent-onboarding.md
  • tests/test_a2a_bus_agent_auth.py
  • tests/test_agent_internal_mint.py
  • tests/test_taosctl_agents_mint.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/cli/taosctl/commands/agents.py
  • tinyagentos/routes/a2a_bus.py
  • tinyagentos/routes/agent_registry.py

Comment thread tinyagentos/routes/agent_registry.py Outdated
- SECURITY: mint-internal refuses to reuse a handle owned by a non-internal
  agent (would have handed it internal scopes + a token); 409 instead.
- Grant expiry is now parsed to a datetime and compared, not string-compared:
  a naive/odd-format timestamp could read an expired grant as live (access
  after revocation). Fail closed on anything unparseable.
- Add a partial unique index on (handle WHERE status='active' AND handle!='')
  so a check-then-register race cannot mint two active rows for one handle;
  mint reuses the winner on IntegrityError. Empty handles stay non-unique.
- Restrict mint scopes to the known VALID_SCOPES (no arbitrary-scope back door).

Kept 403 (not 401) on the no-credential bus-read branch: the middleware already
401s a truly credential-less request, so the only caller reaching that branch is
an authenticated non-admin user, for whom 403 (forbidden) is correct.

Adds tests: 409 origin guard, 422 unknown scope, unique-active-handle index +
empty-handle coexistence, and the grant-expiry parser (naive/past/future/garbage).
# Scopes an operator may grant when minting an internal agent. Mirrors the
# consent-flow VALID_SCOPES; the mint route must not be a back door to invent
# arbitrary scopes that the rest of the system does not understand.
_ALLOWED_SCOPES = frozenset({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: _ALLOWED_SCOPES is a hand-maintained duplicate of VALID_SCOPES in tinyagentos/routes/agent_auth_requests.py:44 (same 8 strings, different module). The comment even acknowledges this: Mirrors the consent-flow VALID_SCOPES. The two lists can drift silently — if a future scope is added in one place, the mint route either keeps minting tokens with the now-invalid scope (drift in the other direction) or blocks legitimate consent-flow scopes. The intent of the previous review (reject unknown scopes) is preserved, but the maintainability risk is the reason VALID_SCOPES exists. Import VALID_SCOPES from agent_auth_requests (or move it to a shared agent_scopes.py) and have both call sites consume the single source of truth.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/agent_registry_store.py Outdated
"ON agent_registry(handle) WHERE status = 'active' AND handle != ''"
)
await conn.commit()
except Exception: # noqa: BLE001 - pre-existing dup handles must not block boot

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: except Exception is too broad: it swallows every error from the CREATE UNIQUE INDEX, not just the specific case the docstring claims (pre-existing duplicate active handles). Disk-full, locked DB, corrupted schema, missing module-level permissions, or a transient aiosqlite.OperationalError will all be logged at WARNING and then silently swallowed — boot continues, but the index does not exist, and the route's IntegrityError handler at agent_registry.py:274 becomes the only thing standing between a concurrent mint and a duplicate row. The bare except is documented as # noqa: BLE001 but the documented narrow case does not match what is actually caught. Recommend catching only the specific failure: except aiosqlite.IntegrityError (or inspect the error message for UNIQUE constraint failed: agent_registry.handle) and re-raise anything else so real problems surface in the operator's logs.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/agent_registry.py Outdated
# same handle (the partial unique index rejected the duplicate).
# Re-read and reuse the row the winner created -- idempotent.
record = await store.get_by_handle(handle)
if record is None or record.get("origin") != _INTERNAL_ORIGIN:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The except IntegrityError branch re-reads by handle, and if the row is missing or owned by a non-internal origin it does a bare raise. The bare raise re-throws the sqlite3.IntegrityError, which FastAPI surfaces as a 500 with a stack trace — but the realistic triggers for this branch (legacy data without the partial index, or a non-mint path somehow owning the handle) are user-facing conditions that deserve a clean 4xx, the same way the existing-handle branch above (line 257) returns 409. Replace raise with raise HTTPException(status_code=409, detail="handle is already owned by a non-internal agent") (or 500 with a controlled message) so the failure mode matches the rest of the function. The PR otherwise fixes the previous-review race condition correctly; this is just polishing the corner case.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/agent_registry_store.py`:
- Around line 121-123: The new unique index on agent handles can cause raw
sqlite3.IntegrityError during active writes, so update the store write paths to
translate those conflicts into the existing domain-level error instead of
letting them bubble up. In the agent_registry_store.py methods that perform
handle writes, especially set_status() and update(), catch the uniqueness
violation around the agent_registry insert/update logic and map it to the
store’s conflict/validation error type so callers see the expected contract
rather than a 500. Use the existing agent_registry_store symbols to keep the fix
localized and consistent with current error handling.
- Around line 119-129: If the unique active-handle index creation in
agent_registry_store initialization fails, do not let
mint-internal/seed-internal proceed as if identity minting is race-safe. Update
the setup flow around the CREATE UNIQUE INDEX block and
_mint_internal_identity() so a failed index install becomes a hard stop or
otherwise prevents internal identity minting from continuing, since the
IntegrityError-based recovery depends on that index being present.
🪄 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: 6bc484ba-2e92-4a60-a577-bc2f0d56a12e

📥 Commits

Reviewing files that changed from the base of the PR and between a9c2cf4 and 4f9121a.

📒 Files selected for processing (5)
  • tests/test_a2a_bus_agent_auth.py
  • tests/test_agent_internal_mint.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/routes/agent_registry.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_agent_internal_mint.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/agent_token_auth.py

Comment thread tinyagentos/agent_registry_store.py Outdated
Comment on lines +119 to +129
try:
await conn.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS uniq_active_handle "
"ON agent_registry(handle) WHERE status = 'active' AND handle != ''"
)
await conn.commit()
except Exception: # noqa: BLE001 - pre-existing dup handles must not block boot
logger.warning(
"could not create uniq_active_handle index (duplicate active handles?)",
exc_info=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don't continue with mint-internal after the unique-handle index fails to install.

_mint_internal_identity() only becomes race-safe because duplicate inserts lose to this index and then recover on IntegrityError. If index creation fails here and init just logs it, concurrent mint-internal/seed-internal calls fall back to the original check-then-register race and can still create duplicate active handles.

🤖 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_registry_store.py` around lines 119 - 129, If the unique
active-handle index creation in agent_registry_store initialization fails, do
not let mint-internal/seed-internal proceed as if identity minting is race-safe.
Update the setup flow around the CREATE UNIQUE INDEX block and
_mint_internal_identity() so a failed index install becomes a hard stop or
otherwise prevents internal identity minting from continuing, since the
IntegrityError-based recovery depends on that index being present.

Comment thread tinyagentos/agent_registry_store.py Outdated
Comment on lines +121 to +123
"CREATE UNIQUE INDEX IF NOT EXISTS uniq_active_handle "
"ON agent_registry(handle) WHERE status = 'active' AND handle != ''"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Handle collisions now need explicit conflict mapping in store writes.

With this index in place, set_status(..., "active") and update(handle=...) can now raise raw sqlite3.IntegrityError whenever another active row already owns the handle. That breaks the current set_status() contract and will surface as 500s unless these conflicts are translated into a domain error.

🤖 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_registry_store.py` around lines 121 - 123, The new unique
index on agent handles can cause raw sqlite3.IntegrityError during active
writes, so update the store write paths to translate those conflicts into the
existing domain-level error instead of letting them bubble up. In the
agent_registry_store.py methods that perform handle writes, especially
set_status() and update(), catch the uniqueness violation around the
agent_registry insert/update logic and map it to the store’s conflict/validation
error type so callers see the expected contract rather than a 500. Use the
existing agent_registry_store symbols to keep the fix localized and consistent
with current error handling.

…ue index

The partial unique handle index fixed the mint race but created wider coupling:
set_status(...,'active') and update(handle=...) could then raise raw
IntegrityError -> 500 on any handle collision, and the migration needed a
too-broad except that swallowed real DB errors (two reviewer Majors + a WARNING).

Replace it with an asyncio lock around the mint's check-then-register. The
controller is a single process, so an in-process lock makes the mint atomic
without a DB-wide constraint -- no blast radius on unrelated handle writes, no
swallowed migration errors. Drop the now-moot IntegrityError handler and the
v2 index migration.

Tests: replace the index tests with a concurrent-mint test (two in-flight mints
for one handle -> one row) and a guard that the mint scope allowlist does not
drift from the canonical VALID_SCOPES.
# an in-process lock is sufficient -- and is preferable to a DB-wide unique
# index on handle, which would make unrelated active-handle writes (set_status,
# update) raise on collision and 500.
_mint_lock = asyncio.Lock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: _mint_lock is a module-level asyncio.Lock, so it only serializes mints within a single uvicorn worker process. The comment on line 137-140 explicitly relies on The controller is a single process to justify dropping the DB unique index — but that assumption is silent in the code: nothing prevents an operator from later launching the controller with --workers 2 (or under gunicorn), at which point two workers can each pass get_by_handle with None, both call register(), and produce two distinct rows with different canonical_ids. Either (a) re-introduce a partial UNIQUE INDEX on handle (the previous approach this PR drops) so the race is closed at the storage layer regardless of worker count, or (b) attach the lock to app.state and document the single-worker requirement loudly in _mint_internal_identity's docstring so a future ops change can't silently regress idempotency.


# The lock makes the check-then-register atomic: a concurrent mint of the
# same handle waits, then sees the row the first one created and reuses it.
async with _mint_lock:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The lock is scoped only around get_by_handle + register (lines 260-282), but the inline comment on lines 258-259 promises The lock makes the check-then-register atomic and the function docstring (lines 250-253) advertises Register-or-reuse ... and mint a registry JWT as a single idempotent operation. The grants loop (lines 285-286) and mint_registry_token (lines 288-294) actually run outside the lock, so a concurrent caller observing the row mid-mint will receive created=False, skip the first caller's grants, and then independently call add_grant + mint_registry_token against the same canonical_id. That happens to be safe today only because add_grant is idempotent and the token is per-call — but it contradicts the docstring and leaves a foot-gun for any future caller that adds a non-idempotent side-effect inside the lock-protected section. Either extend async with _mint_lock: to cover the full get_by_handleadd_grantmint_registry_token sequence (cheap, and matches what the docstring says) or rewrite the comment to say exactly which steps are serialized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_agent_internal_mint.py (1)

132-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Force the race window instead of relying on gather.

This can still pass if the requests end up serialized by the test transport, so it may miss a future regression in the check-then-register path. Prefer a barrier/patch around get_by_handle or register so both requests are guaranteed to overlap before the row is created.

🤖 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 `@tests/test_agent_internal_mint.py` around lines 132 - 145, The concurrent
mint test currently depends on asyncio.gather alone, which may serialize through
the test transport and fail to exercise the race in the check-then-register
flow. Update test_concurrent_mint_same_handle_makes_one_row in
tests/test_agent_internal_mint.py to force overlap by adding a barrier or
patching the agent registry path around get_by_handle or register so both
mint_internal requests block until both are in-flight before creating the row.
Keep the same assertions on canonical_id and single row, but make the race
deterministic.
🤖 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.

Nitpick comments:
In `@tests/test_agent_internal_mint.py`:
- Around line 132-145: The concurrent mint test currently depends on
asyncio.gather alone, which may serialize through the test transport and fail to
exercise the race in the check-then-register flow. Update
test_concurrent_mint_same_handle_makes_one_row in
tests/test_agent_internal_mint.py to force overlap by adding a barrier or
patching the agent registry path around get_by_handle or register so both
mint_internal requests block until both are in-flight before creating the row.
Keep the same assertions on canonical_id and single row, but make the race
deterministic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34e4bf28-632e-451b-999e-83fb7fb76e7d

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9121a and ec17c6c.

📒 Files selected for processing (3)
  • tests/test_agent_internal_mint.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/routes/agent_registry.py
💤 Files with no reviewable changes (1)
  • tinyagentos/agent_registry_store.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant