feat(auth): per-agent registry-identity auth for the A2A bus read proxy (#138 Phase 1) - #1507
Conversation
…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 reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughAdds 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. ChangesAgent JWT Auth for A2A Bus + Internal Mint/Seed
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
| 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. |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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") | ||
|
|
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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 | |||
|
|
|||
|
|
|||
There was a problem hiding this comment.
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.
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (3 files changed since previous review)
Previously flagged issues — status
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
Issue Details (click to expand)SUGGESTION
WARNING
Files Reviewed (5 files changed since previous review)
Previously flagged issues — status
Fix these issues in Kilo Cloud Previous review (commit a9c2cf4)Status: 5 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (11 files)
Reviewed by minimax-m3 · Input: 50.8K · Output: 5.7K · Cached: 536.3K |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tinyagentos/agent_token_auth.py (1)
76-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSuppress exception chaining for the 401 mapping.
Line 77 triggers Ruff B904; use
from Noneso 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
📒 Files selected for processing (11)
README.mddocs/design/external-agent-onboarding.mdtests/test_a2a_bus_agent_auth.pytests/test_agent_internal_mint.pytests/test_taosctl_agents_mint.pytinyagentos/agent_registry_store.pytinyagentos/agent_token_auth.pytinyagentos/auth_middleware.pytinyagentos/cli/taosctl/commands/agents.pytinyagentos/routes/a2a_bus.pytinyagentos/routes/agent_registry.py
- 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({ |
There was a problem hiding this comment.
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.
| "ON agent_registry(handle) WHERE status = 'active' AND handle != ''" | ||
| ) | ||
| await conn.commit() | ||
| except Exception: # noqa: BLE001 - pre-existing dup handles must not block boot |
There was a problem hiding this comment.
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.
| # 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
tests/test_a2a_bus_agent_auth.pytests/test_agent_internal_mint.pytinyagentos/agent_registry_store.pytinyagentos/agent_token_auth.pytinyagentos/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
| 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, | ||
| ) |
There was a problem hiding this comment.
🗄️ 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.
| "CREATE UNIQUE INDEX IF NOT EXISTS uniq_active_handle " | ||
| "ON agent_registry(handle) WHERE status = 'active' AND handle != ''" | ||
| ) |
There was a problem hiding this comment.
🗄️ 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() |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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_handle → add_grant → mint_registry_token sequence (cheap, and matches what the docstring says) or rewrite the comment to say exactly which steps are serialized.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_agent_internal_mint.py (1)
132-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winForce 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_handleorregisterso 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
📒 Files selected for processing (3)
tests/test_agent_internal_mint.pytinyagentos/agent_registry_store.pytinyagentos/routes/agent_registry.py
💤 Files with no reviewable changes (1)
- tinyagentos/agent_registry_store.py
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
GET /api/a2a/bus/channels|messages) withAuthorization: Bearer <registry-JWT>, authorized by ana2a_receivescope grant. No owner session/password needed.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_tokennow delegates to it (feed routes unchanged)._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.POST /api/agents/registry/mint-internal+/seed-internal, andtaosctl agents mint|seed-internal, to mint the 4 identities (idempotent by handle;get_by_handleadded to the registry store).Reviewer notes
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 touser_id is not None).fromto the verified identity, verify-and-warn -> enforce) is Phase 2 in the taosmd repo; this PR is controller read-auth + identity minting only.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
agents mintandseed-internal.