Skip to content

tsk-5bup3r [OPEN] Registry tokens never expire: taOS mints JWTs with - #2235

Draft
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-5bup3r
Draft

tsk-5bup3r [OPEN] Registry tokens never expire: taOS mints JWTs with#2235
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-5bup3r

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Autonomous build of board card tsk-5bup3r.

Files:
tests/test_agent_registry.py | 38 ++++++++++++
tests/test_agent_registry_store.py | 114 ++++++++++++++++++++++++++++++++++-
tinyagentos/agent_registry_store.py | 92 ++++++++++++++++++++++------
tinyagentos/agent_token_auth.py | 11 +++-
tinyagentos/config.py | 4 ++
tinyagentos/routes/agent_registry.py | 31 +++++++++-
6 files changed, 269 insertions(+), 21 deletions(-)

Summary by CodeRabbit

  • New Features

    • Registry tokens now expire after a configurable lifetime, defaulting to 24 hours.
    • Added token renewal through a dedicated API endpoint.
    • Renewed tokens preserve agent identity and metadata.
    • Legacy tokens without expiration claims can be supported during a configurable migration period.
  • Bug Fixes

    • Expired, malformed, invalidly signed, and unauthorized tokens are now rejected appropriately.
    • Added validation for registry slug lookups and token claims.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Registry token lifecycle

Layer / File(s) Summary
Token expiration and verification
tinyagentos/agent_registry_store.py, tinyagentos/agent_token_auth.py, tinyagentos/config.py, tests/test_agent_registry_store.py, tests/test_agent_registry.py
Registry tokens now include expiration claims. Verification handles expired and legacy tokens. Configuration supplies token lifetime and migration cutoff values.
Token renewal endpoint
tinyagentos/routes/agent_registry.py, tests/test_agent_registry.py, tests/test_agent_registry_store.py
The renewal endpoint accepts bearer tokens, preserves token claims, validates the replacement token, checks agent status, and returns authentication or authorization errors when required.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: hognek

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant renew_registry_token_route
  participant renew_registry_token
  participant AgentRegistry
  Client->>renew_registry_token_route: POST bearer token
  renew_registry_token_route->>renew_registry_token: Renew signed token
  renew_registry_token->>renew_registry_token_route: Return replacement token
  renew_registry_token_route->>AgentRegistry: Check agent is active
  AgentRegistry->>renew_registry_token_route: Return registry status
  renew_registry_token_route->>Client: Return replacement token or error
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.13% 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 identifies the main change: adding expiration support to registry JWTs.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-5bup3r

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.

@gitar-bot

gitar-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-kilo review

VERDICT: The changes implement token expiration (exp claim), renewal endpoint, and migration window — mostly solid but with several correctness and security concerns.

  • Security: Token renewal accepts expired tokens without checking agent status first (tinyagentos/routes/agent_registry.py:765-774). The renew_registry_token call verifies signature only (via _verify_signature_only), which explicitly skips exp check. The agent status check happens after renewal. An attacker with a stolen expired token could renew it even if the agent was deactivated between token expiry and renewal call. Fix: check agent status before calling renew_registry_token.

  • Security: Migration window uses server time without skew tolerance (tinyagentos/agent_registry_store.py:378-385). allow_no_exp_until compares against time.time() directly. If server clock drifts or is set incorrectly, tokens without exp could be rejected prematurely or accepted too long. Consider adding a small skew buffer (e.g., ±60s) or document that NTP is required.

  • Correctness: renew_registry_token doesn't validate sub exists in payload (tinyagentos/agent_registry_store.py:400-418). It assumes payload["sub"] is present. If a malformed but validly-signed token lacks sub, this will KeyError. Add defensive check or document that only registry-minted tokens are expected.

  • Correctness: renew_registry_token preserves project_id as None when absent (tinyagentos/agent_registry_store.py:413). payload.get("project_id") returns None if missing, which mint_registry_token then includes as "project_id": null in the new token. Original tokens omit the key entirely. This changes the token structure. Fix: only pass project_id if present in original.

  • Test gap: No test for renew_registry_token with missing optional claims (tests/test_agent_registry_store.py). The renewal tests only cover tokens with all fields. Should test renewal of minimal token (only required claims).

  • Test gap: No test for clock skew during migration window (tests/test_agent_registry_store.py:330-355). The migration tests use exact cutoff boundaries. Real deployments need skew tolerance.

  • Style: _verify_signature_only duplicates JWT parsing logic (tinyagentos/agent_registry_store.py:283-312). It reimplements header/payload splitting and base64url decode that verify_registry_token also does. Could extract shared parsing to avoid drift.

  • Config: registry_token_migration_cutoff_ts loaded as float but config may provide int (tinyagentos/config.py:203). float(data["registry_token_migration_cutoff_ts"]) works for both but explicit float() on int is fine. No issue.

  • Missing test: Renewal endpoint with deactivated agent (tests/test_agent_registry.py). Should verify 403 when agent status != active.
    VERDICT: The changes implement token expiration (exp claim), renewal endpoint, and migration window — mostly solid but with several correctness and security concerns.

  • Security: Token renewal accepts expired tokens without checking agent status first (tinyagentos/routes/agent_registry.py:765-774). The renew_registry_token call verifies signature only (via _verify_signature_only), which explicitly skips exp check. The agent status check happens after renewal. An attacker with a stolen expired token could renew it even if the agent was deactivated between token expiry and renewal call. Fix: check agent status before calling renew_registry_token.

  • Security: Migration window uses server time without skew tolerance (tinyagentos/agent_registry_store.py:378-385). allow_no_exp_until compares against time.time() directly. If server clock drifts or is set incorrectly, tokens without exp could be rejected prematurely or accepted too long. Consider adding a small skew buffer (e.g., ±60s) or document that NTP is required.

  • Correctness: renew_registry_token doesn't validate sub exists in payload (tinyagentos/agent_registry_store.py:400-418). It assumes payload["sub"] is present. If a malformed but validly-signed token lacks sub, this will KeyError. Add defensive check or document that only registry-minted tokens are expected.

  • Correctness: renew_registry_token preserves project_id as None when absent (tinyagentos/agent_registry_store.py:413). payload.get("project_id") returns None if missing, which mint_registry_token then includes as "project_id": null in the new token. Original tokens omit the key entirely. This changes the token structure. Fix: only pass project_id if present in original.

  • Test gap: No test for renew_registry_token with missing optional claims (tests/test_agent_registry_store.py). The renewal tests only cover tokens with all fields. Should test renewal of minimal token (only required claims).

  • Test gap: No test for clock skew during migration window (tests/test_agent_registry_store.py:330-355). The migration tests use exact cutoff boundaries. Real deployments need skew tolerance.

  • Style: _verify_signature_only duplicates JWT parsing logic (tinyagentos/agent_registry_store.py:283-312). It reimplements header/payload splitting and base64url decode that verify_registry_token also does. Could extract shared parsing to avoid drift.

  • Config: registry_token_migration_cutoff_ts loaded as float but config may provide int (tinyagentos/config.py:203). float(data["registry_token_migration_cutoff_ts"]) works for both but explicit float() on int is fine. No issue.

  • Missing test: Renewal endpoint with deactivated agent (tests/test_agent_registry.py). Should verify 403 when agent status != active.

Automated first-pass review by the nemotron-ultra-kilo lane. The lead still reviews before merge.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add exp claim to registry JWTs and a self-service token renewal endpoint

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add exp to minted registry JWTs and enforce expiry during verification.
• Allow a temporary migration window for legacy tokens missing exp.
• Add a renewal API so agents can rotate an expired token if still active.
Diagram

sequenceDiagram
    participant Agent as Agent
    participant Route as "token/renew route"
    participant Store as "agent_registry_store.py"
    participant Auth as "agent_token_auth.py"
    participant Registry as "AgentRegistryStore"

    Agent->>Route: POST token/renew (Bearer expired token)
    Route->>Store: renew_registry_token(token, pub, priv)
    Store->>Store: verify signature only (ignore exp)
    Store->>Store: mint_registry_token(new exp)
    Store-->>Route: new signed JWT
    Route->>Registry: check agent status == active
    Registry-->>Route: record
    Route-->>Agent: 200 new token

    Agent->>Auth: request with Bearer token
    Auth->>Store: verify_registry_token(token, allow_no_exp_until)
    Store-->>Auth: payload or ValueError (expired/no exp)
Loading
High-Level Assessment

The approach (add exp + enforce expiry, provide a bounded migration window for legacy tokens, and add a signature-only renewal endpoint) is the most practical retrofit for an already-deployed token scheme. It closes the ‘never expires’ security gap without a hard cutover that would immediately break existing agents, and avoids the operational complexity of maintaining server-side refresh tokens or revocation lists beyond the existing registry status checks.

Files changed (6) +269 / -21

Enhancement (3) +114 / -20
agent_registry_store.pyAdd exp-based expiry, migration handling, and token renewal helper +75/-17

Add exp-based expiry, migration handling, and token renewal helper

• Adds a default registry token lifetime (24h) and stamps 'exp' on minted tokens with optional 'lifetime_seconds'. Refactors signature verification into '_verify_signature_only', updates 'verify_registry_token' to enforce expiry and fail closed on missing 'exp' after a configurable grace window, and adds 'renew_registry_token' that re-mints a token from an existing signature-valid token even if expired.

tinyagentos/agent_registry_store.py

agent_token_auth.pyWire migration cutoff into agent token verification +9/-2

Wire migration cutoff into agent token verification

• Adds '_get_migration_cutoff' to read 'registry_token_migration_cutoff_ts' from app config. Passes 'allow_no_exp_until' into 'verify_registry_token' in both '_verify_agent_scope' and 'check_agent_identity' so legacy tokens remain valid only within the migration window.

tinyagentos/agent_token_auth.py

agent_registry.pyAdd POST /api/agents/registry/token/renew endpoint +30/-1

Add POST /api/agents/registry/token/renew endpoint

• Adds a renewal route that requires an Authorization Bearer token, calls 'renew_registry_token' (accepting expired-but-signed tokens), then verifies the renewed token and ensures the referenced agent is still active in the registry before returning the new token.

tinyagentos/routes/agent_registry.py

Tests (2) +151 / -1
test_agent_registry.pyTest exp claim and token renewal API behavior +38/-0

Test exp claim and token renewal API behavior

• Adds assertions that minted tokens include an 'exp' claim greater than 'iat'. Adds integration tests for 'POST /api/agents/registry/token/renew' (happy path returns a different token with same identity claims; rejects missing Bearer and malformed tokens).

tests/test_agent_registry.py

test_agent_registry_store.pyAdd unit tests for token expiry, migration window, and renewal +113/-1

Add unit tests for token expiry, migration window, and renewal

• Introduces tests verifying 'exp' presence, rejecting expired tokens, and the allow-no-exp migration window behavior (accept before cutoff, reject after). Adds renewal tests ensuring claim preservation and rejecting renewal with an invalid public key signature check.

tests/test_agent_registry_store.py

Other (1) +4 / -0
config.pyAdd config knobs for registry token lifetime and migration cutoff +4/-0

Add config knobs for registry token lifetime and migration cutoff

• Extends 'AppConfig' and 'load_config' with 'registry_token_lifetime_seconds' (default 86400) and optional 'registry_token_migration_cutoff_ts' for controlling the legacy no-exp acceptance window.

tinyagentos/config.py

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Renew bypasses exp migration 🐞 Bug ⛨ Security
Description
renew_registry_token() re-mints tokens after verifying only the signature, so a legacy token
without exp can be exchanged for a fresh exp token even after the migration cutoff would have
rejected it. This defeats the cutoff policy intended to retire non-expiring tokens.
Code

tinyagentos/agent_registry_store.py[R403-406]

+    payload = _verify_signature_only(token, public_key_pem)
+    return mint_registry_token(
+        payload["sub"],
+        private_key_pem,
Relevance

●●● Strong

Team often accepts security hardening (e.g., SSRF redirect fix accepted). Likely accept
migration-cutoff bypass.

PR-#304
PR-#301

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
verify_registry_token() enforces missing-exp rejection after the cutoff, but
renew_registry_token() never checks exp at all, so it can mint a new token from a no-exp
legacy token regardless of cutoff.

tinyagentos/agent_registry_store.py[365-386]
tinyagentos/agent_registry_store.py[389-411]
tinyagentos/routes/agent_registry.py[747-773]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The renewal path accepts legacy tokens with no `exp` forever because it uses `_verify_signature_only()` and never applies the migration cutoff policy.

### Issue Context
The renewal path is intentionally allowed to accept *expired* tokens, but it should not allow *no-exp* legacy tokens past the configured migration cutoff.

### Fix Focus Areas
- tinyagentos/agent_registry_store.py[283-412]
- tinyagentos/routes/agent_registry.py[747-773]
- tinyagentos/config.py[52-214]

### Suggested fix
- Extend `renew_registry_token()` to accept `allow_no_exp_until: float | None` (or `migration_cutoff_ts`) and:
 - verify signature,
 - require that the token either has an `exp` claim OR `now < allow_no_exp_until`.
 - do **not** require the token to be unexpired.
- In `renew_registry_token_route`, pass `request.app.state.config.registry_token_migration_cutoff_ts` into `renew_registry_token()`.
- Add a test covering: a legacy no-`exp` token cannot be renewed after the cutoff.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. /token/renew not allowlisted 📜 Skill insight ⛨ Security
Description
A new agent-facing route POST /api/agents/registry/token/renew was added but is not present in the
agent-token allowlist, so registry JWT calls can be blocked by auth middleware and return 401. This
violates the requirement to add new agent/JWT-accessible routes to tinyagentos/auth_middleware.py.
Code

tinyagentos/routes/agent_registry.py[R747-748]

+@router.post("/api/agents/registry/token/renew")
+async def renew_registry_token_route(request: Request):
Relevance

●● Moderate

No specific precedent for auth allowlist omissions; security issues often accepted but
route-allowlist rule not evidenced.

PR-#304

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires that new agent/JWT-accessible (method, path) routes be added to the auth
middleware allowlist. The PR adds POST /api/agents/registry/token/renew, while the allowlist
_AGENT_TOKEN_PATHS is composed from sets that do not include this new path.

tinyagentos/routes/agent_registry.py[747-774]
tinyagentos/auth_middleware.py[14-51]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`POST /api/agents/registry/token/renew` is intended to be called with a registry Bearer JWT, but the auth middleware only passes registry JWTs through for explicitly allowlisted paths.

## Issue Context
The PR introduces a new route at `/api/agents/registry/token/renew` that requires a Bearer token, but `_AGENT_TOKEN_PATHS` in `tinyagentos/auth_middleware.py` does not include this path.

## Fix Focus Areas
- tinyagentos/auth_middleware.py[14-51]
- tinyagentos/routes/agent_registry.py[747-774]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Route imports agent_registry_store 📜 Skill insight ⌂ Architecture
Description
tinyagentos/routes/agent_registry.py directly imports from tinyagentos.agent_registry_store,
violating the rule that routes must access stores via request.app.state rather than direct imports
of store modules. This increases coupling and bypasses the intended dependency-injection pattern.
Code

tinyagentos/routes/agent_registry.py[32]

+from tinyagentos.agent_registry_store import mint_registry_token, renew_registry_token, verify_registry_token
Relevance

●● Moderate

Past suggestions to use request.app.state over direct access exist but outcome mostly undetermined.

PR-#248

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule forbids routes from directly importing store modules/classes, requiring stores
be accessed through request.app.state. The modified import line in
tinyagentos/routes/agent_registry.py imports from the store module directly.

tinyagentos/routes/agent_registry.py[32-33]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Route modules under `tinyagentos/routes/` must not directly import store modules; they should obtain store instances via `request.app.state`.

## Issue Context
This PR expands the direct import from `tinyagentos.agent_registry_store` by adding `renew_registry_token` and `verify_registry_token` to the import list.

## Fix Focus Areas
- tinyagentos/routes/agent_registry.py[32-33]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Legacy tokens break immediately 🐞 Bug ☼ Reliability
Description
check_agent_scope()/check_agent_identity() now enforce exp via verify_registry_token(), but
the default registry_token_migration_cutoff_ts=None means any legacy registry token without exp
is rejected (401) unless an operator explicitly configures a future cutoff. This is a
backward-compatibility/availability regression on upgrade for deployments with pre-exp tokens.
Code

tinyagentos/agent_token_auth.py[R104-107]

    _private_pem, public_pem = _get_keypair(request)
    try:
-        payload = verify_registry_token(raw_token, public_pem)
+        payload = verify_registry_token(raw_token, public_pem, allow_no_exp_until=_get_migration_cutoff(request))
    except ValueError:
Relevance

●● Moderate

No clear history on migration-default/backward-compat behavior; reliability fixes sometimes accepted
but evidence not specific.

PR-#474

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Authentication now passes allow_no_exp_until from config; when it is None,
verify_registry_token() rejects tokens that have no exp. The config default is None, and the
module docstring indicates legacy tokens had no exp claim, so upgrades will fail unless a cutoff is
set.

tinyagentos/agent_token_auth.py[39-44]
tinyagentos/agent_token_auth.py[103-109]
tinyagentos/agent_registry_store.py[365-386]
tinyagentos/config.py[52-67]
tinyagentos/agent_token_auth.py[13-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Legacy registry tokens that lack an `exp` claim will be rejected immediately after upgrade because `registry_token_migration_cutoff_ts` defaults to `None`, causing `verify_registry_token()` to raise `ValueError("token has no exp claim")`.

### Issue Context
The PR adds a migration-window mechanism (`allow_no_exp_until`) but leaves the default configuration with no window. The existing module documentation indicates tokens previously carried no `exp` claim, so this is likely to break existing agents.

### Fix Focus Areas
- tinyagentos/config.py[52-214]
- tinyagentos/agent_token_auth.py[39-109]
- tinyagentos/agent_registry_store.py[365-386]

### Suggested fix
- In `load_config()`, if `registry_token_migration_cutoff_ts` is absent, set it once to a reasonable future timestamp (e.g., `time.time() + 30*86400`) and call `save_config(cfg, path)` similar to the existing litellm_port pin migration.
- Add/adjust tests to ensure legacy no-`exp` tokens authenticate during the grace window by default on existing installs (or clearly document/enforce the requirement to set the cutoff explicitly).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Config fields not saved 🐞 Bug ⚙ Maintainability
Description
load_config() reads registry_token_lifetime_seconds and registry_token_migration_cutoff_ts,
but AppConfig.to_dict() omits them, so any save_config() will silently drop these settings from
config.yaml. This can unexpectedly revert a configured migration cutoff (and lifetime) after
unrelated config-saving migrations.
Code

tinyagentos/config.py[R199-200]

+        registry_token_lifetime_seconds=int(data.get("registry_token_lifetime_seconds", 86400)),
+        registry_token_migration_cutoff_ts=float(data["registry_token_migration_cutoff_ts"]) if "registry_token_migration_cutoff_ts" in data else None,
Relevance

●● Moderate

No prior evidence on AppConfig to_dict completeness; could be treated as minor unless it breaks
migrations.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new fields are loaded from YAML, but serialization omits them and save_config() uses that
serialization directly, so saving config will drop them.

tinyagentos/config.py[186-201]
tinyagentos/config.py[69-88]
tinyagentos/config.py[345-349]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
New registry token config settings are not round-trippable: they are loaded but not serialized.

### Issue Context
`save_config()` writes `yaml.dump(config.to_dict())`, so any omitted fields are lost when config is persisted.

### Fix Focus Areas
- tinyagentos/config.py[69-88]
- tinyagentos/config.py[186-214]
- tinyagentos/config.py[345-349]

### Suggested fix
- Update `AppConfig.to_dict()` to include:
 - `registry_token_lifetime_seconds` (at least when != default)
 - `registry_token_migration_cutoff_ts` (when not None)
- Add a unit test that loads a config containing these keys, saves it, and asserts the keys remain present with the same values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Token lifetime config unused 🐞 Bug ≡ Correctness
Description
registry_token_lifetime_seconds is added and parsed into AppConfig, but minting/renewal call
sites never pass it, so configuring token lifetime has no effect. Tokens always use
DEFAULT_REGISTRY_TOKEN_LIFETIME unless callers explicitly override lifetime_seconds.
Code

tinyagentos/config.py[R65-66]

+    registry_token_lifetime_seconds: int = 86400
+    registry_token_migration_cutoff_ts: float | None = None
Relevance

●● Moderate

No historical evidence on unused config fields; similar “dead config/state” issues show mixed
enforcement.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The config field exists and is loaded, but a repo-wide search shows no usage beyond
definition/loading; minting defaults to a constant and key production routes mint tokens without
passing lifetime_seconds.

tinyagentos/config.py[52-67]
tinyagentos/config.py[186-201]
tinyagentos/agent_registry_store.py[310-351]
tinyagentos/routes/agent_registry.py[242-285]
tinyagentos/routes/agent_registry.py[287-349]
tinyagentos/routes/agent_auth_requests.py[449-455]
tinyagentos/routes/agent_auth_requests.py[537-543]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new `registry_token_lifetime_seconds` config knob is dead: minted and renewed tokens ignore it.

### Issue Context
`mint_registry_token()` supports `lifetime_seconds`, but production call sites do not pass a value.

### Fix Focus Areas
- tinyagentos/routes/agent_registry.py[242-285]
- tinyagentos/routes/agent_registry.py[287-349]
- tinyagentos/routes/agent_registry.py[747-773]
- tinyagentos/routes/agent_auth_requests.py[449-455]
- tinyagentos/routes/agent_auth_requests.py[537-543]
- tinyagentos/agent_registry_store.py[310-363]

### Suggested fix
- Read `lifetime = request.app.state.config.registry_token_lifetime_seconds` (with sane validation, e.g. `>= 60`) and pass `lifetime_seconds=lifetime` to:
 - `/api/agents/registry/register` mint
 - `_mint_internal_identity` mint
 - consent/approval mint paths in `agent_auth_requests.py`
 - `/api/agents/registry/token/renew` renewal (pass through to `renew_registry_token(..., lifetime_seconds=lifetime)`)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. renew_registry_token_route returns dict 📜 Skill insight ✧ Quality
Description
The new renew_registry_token_route returns a raw dict ({"token": new_token}) and does not
declare a response_model. This violates the requirement to use Pydantic models for route response
payloads.
Code

tinyagentos/routes/agent_registry.py[773]

+    return {"token": new_token}
Relevance

● Weak

Similar “use Pydantic response_model” route suggestion was explicitly rejected by reviewers.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires Pydantic models for route responses. The new route is declared without a
response_model and returns a plain dict containing the token.

tinyagentos/routes/agent_registry.py[747-774]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Route responses should use Pydantic models via `response_model=...` rather than returning raw dicts.

## Issue Context
`POST /api/agents/registry/token/renew` currently returns `{"token": new_token}` without a declared response schema.

## Fix Focus Areas
- tinyagentos/routes/agent_registry.py[747-774]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +747 to +748
@router.post("/api/agents/registry/token/renew")
async def renew_registry_token_route(request: Request):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. /token/renew not allowlisted 📜 Skill insight ⛨ Security

A new agent-facing route POST /api/agents/registry/token/renew was added but is not present in the
agent-token allowlist, so registry JWT calls can be blocked by auth middleware and return 401. This
violates the requirement to add new agent/JWT-accessible routes to tinyagentos/auth_middleware.py.
Agent Prompt
## Issue description
`POST /api/agents/registry/token/renew` is intended to be called with a registry Bearer JWT, but the auth middleware only passes registry JWTs through for explicitly allowlisted paths.

## Issue Context
The PR introduces a new route at `/api/agents/registry/token/renew` that requires a Bearer token, but `_AGENT_TOKEN_PATHS` in `tinyagentos/auth_middleware.py` does not include this path.

## Fix Focus Areas
- tinyagentos/auth_middleware.py[14-51]
- tinyagentos/routes/agent_registry.py[747-774]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

from pydantic import BaseModel, field_validator

from tinyagentos.agent_registry_store import mint_registry_token
from tinyagentos.agent_registry_store import mint_registry_token, renew_registry_token, verify_registry_token

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Route imports agent_registry_store 📜 Skill insight ⌂ Architecture

tinyagentos/routes/agent_registry.py directly imports from tinyagentos.agent_registry_store,
violating the rule that routes must access stores via request.app.state rather than direct imports
of store modules. This increases coupling and bypasses the intended dependency-injection pattern.
Agent Prompt
## Issue description
Route modules under `tinyagentos/routes/` must not directly import store modules; they should obtain store instances via `request.app.state`.

## Issue Context
This PR expands the direct import from `tinyagentos.agent_registry_store` by adding `renew_registry_token` and `verify_registry_token` to the import list.

## Fix Focus Areas
- tinyagentos/routes/agent_registry.py[32-33]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 104 to 107
_private_pem, public_pem = _get_keypair(request)
try:
payload = verify_registry_token(raw_token, public_pem)
payload = verify_registry_token(raw_token, public_pem, allow_no_exp_until=_get_migration_cutoff(request))
except ValueError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Legacy tokens break immediately 🐞 Bug ☼ Reliability

check_agent_scope()/check_agent_identity() now enforce exp via verify_registry_token(), but
the default registry_token_migration_cutoff_ts=None means any legacy registry token without exp
is rejected (401) unless an operator explicitly configures a future cutoff. This is a
backward-compatibility/availability regression on upgrade for deployments with pre-exp tokens.
Agent Prompt
### Issue description
Legacy registry tokens that lack an `exp` claim will be rejected immediately after upgrade because `registry_token_migration_cutoff_ts` defaults to `None`, causing `verify_registry_token()` to raise `ValueError("token has no exp claim")`.

### Issue Context
The PR adds a migration-window mechanism (`allow_no_exp_until`) but leaves the default configuration with no window. The existing module documentation indicates tokens previously carried no `exp` claim, so this is likely to break existing agents.

### Fix Focus Areas
- tinyagentos/config.py[52-214]
- tinyagentos/agent_token_auth.py[39-109]
- tinyagentos/agent_registry_store.py[365-386]

### Suggested fix
- In `load_config()`, if `registry_token_migration_cutoff_ts` is absent, set it once to a reasonable future timestamp (e.g., `time.time() + 30*86400`) and call `save_config(cfg, path)` similar to the existing litellm_port pin migration.
- Add/adjust tests to ensure legacy no-`exp` tokens authenticate during the grace window by default on existing installs (or clearly document/enforce the requirement to set the cutoff explicitly).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +403 to +406
payload = _verify_signature_only(token, public_key_pem)
return mint_registry_token(
payload["sub"],
private_key_pem,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Renew bypasses exp migration 🐞 Bug ⛨ Security

renew_registry_token() re-mints tokens after verifying only the signature, so a legacy token
without exp can be exchanged for a fresh exp token even after the migration cutoff would have
rejected it. This defeats the cutoff policy intended to retire non-expiring tokens.
Agent Prompt
### Issue description
The renewal path accepts legacy tokens with no `exp` forever because it uses `_verify_signature_only()` and never applies the migration cutoff policy.

### Issue Context
The renewal path is intentionally allowed to accept *expired* tokens, but it should not allow *no-exp* legacy tokens past the configured migration cutoff.

### Fix Focus Areas
- tinyagentos/agent_registry_store.py[283-412]
- tinyagentos/routes/agent_registry.py[747-773]
- tinyagentos/config.py[52-214]

### Suggested fix
- Extend `renew_registry_token()` to accept `allow_no_exp_until: float | None` (or `migration_cutoff_ts`) and:
  - verify signature,
  - require that the token either has an `exp` claim OR `now < allow_no_exp_until`.
  - do **not** require the token to be unexpired.
- In `renew_registry_token_route`, pass `request.app.state.config.registry_token_migration_cutoff_ts` into `renew_registry_token()`.
- Add a test covering: a legacy no-`exp` token cannot be renewed after the cutoff.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/config.py
Comment on lines +65 to +66
registry_token_lifetime_seconds: int = 86400
registry_token_migration_cutoff_ts: float | None = 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.

Remediation recommended

6. Token lifetime config unused 🐞 Bug ≡ Correctness

registry_token_lifetime_seconds is added and parsed into AppConfig, but minting/renewal call
sites never pass it, so configuring token lifetime has no effect. Tokens always use
DEFAULT_REGISTRY_TOKEN_LIFETIME unless callers explicitly override lifetime_seconds.
Agent Prompt
### Issue description
The new `registry_token_lifetime_seconds` config knob is dead: minted and renewed tokens ignore it.

### Issue Context
`mint_registry_token()` supports `lifetime_seconds`, but production call sites do not pass a value.

### Fix Focus Areas
- tinyagentos/routes/agent_registry.py[242-285]
- tinyagentos/routes/agent_registry.py[287-349]
- tinyagentos/routes/agent_registry.py[747-773]
- tinyagentos/routes/agent_auth_requests.py[449-455]
- tinyagentos/routes/agent_auth_requests.py[537-543]
- tinyagentos/agent_registry_store.py[310-363]

### Suggested fix
- Read `lifetime = request.app.state.config.registry_token_lifetime_seconds` (with sane validation, e.g. `>= 60`) and pass `lifetime_seconds=lifetime` to:
  - `/api/agents/registry/register` mint
  - `_mint_internal_identity` mint
  - consent/approval mint paths in `agent_auth_requests.py`
  - `/api/agents/registry/token/renew` renewal (pass through to `renew_registry_token(..., lifetime_seconds=lifetime)`)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/config.py
Comment on lines +199 to +200
registry_token_lifetime_seconds=int(data.get("registry_token_lifetime_seconds", 86400)),
registry_token_migration_cutoff_ts=float(data["registry_token_migration_cutoff_ts"]) if "registry_token_migration_cutoff_ts" in data else 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.

Action required

7. Config fields not saved 🐞 Bug ⚙ Maintainability

load_config() reads registry_token_lifetime_seconds and registry_token_migration_cutoff_ts,
but AppConfig.to_dict() omits them, so any save_config() will silently drop these settings from
config.yaml. This can unexpectedly revert a configured migration cutoff (and lifetime) after
unrelated config-saving migrations.
Agent Prompt
### Issue description
New registry token config settings are not round-trippable: they are loaded but not serialized.

### Issue Context
`save_config()` writes `yaml.dump(config.to_dict())`, so any omitted fields are lost when config is persisted.

### Fix Focus Areas
- tinyagentos/config.py[69-88]
- tinyagentos/config.py[186-214]
- tinyagentos/config.py[345-349]

### Suggested fix
- Update `AppConfig.to_dict()` to include:
  - `registry_token_lifetime_seconds` (at least when != default)
  - `registry_token_migration_cutoff_ts` (when not None)
- Add a unit test that loads a config containing these keys, saves it, and asserts the keys remain present with the same values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@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: 5

🤖 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 389-411: Update renew_registry_token to retrieve the subject claim
with payload.get("sub", "") instead of direct payload["sub"] access, matching
the defensive pattern used by other verification call sites and ensuring
malformed signed payloads raise ValueError through mint_registry_token rather
than KeyError.
- Around line 389-411: Bound renew_registry_token so a signed token cannot be
renewed indefinitely after expiration: enforce a maximum grace period relative
to its exp (using the existing registry lifetime configuration where
appropriate) and reject tokens beyond that window before minting the
replacement. Keep valid unexpired tokens and recently expired tokens eligible,
while preserving the existing claim reuse and minting behavior.

In `@tinyagentos/config.py`:
- Around line 199-200: Update the config loading logic for
registry_token_lifetime_seconds and registry_token_migration_cutoff_ts to catch
invalid numeric values, log a warning, and use their respective defaults instead
of allowing ValueError or TypeError to escape. Follow the existing defensive
parsing pattern in load_config and preserve None when the migration cutoff key
is absent.
- Around line 65-66: Update AppConfig.to_dict() to explicitly serialize
registry_token_lifetime_seconds and registry_token_migration_cutoff_ts, using
the same conditional/default-comparison pattern as memory_url so custom values
persist through save_config() and save_config_locked() while defaults remain
omitted.

In `@tinyagentos/routes/agent_registry.py`:
- Around line 747-775: Update the registry token route call sites to pass
request.app.state.config.registry_token_lifetime_seconds as lifetime_seconds:
use it when minting in the /api/agents/registry/register handler and when
renewing in renew_registry_token_route. Preserve the existing token validation
and response behavior.
🪄 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: 733001c2-5533-4055-81ed-3d7890127dc0

📥 Commits

Reviewing files that changed from the base of the PR and between d1fc599 and 3e9173e.

📒 Files selected for processing (6)
  • tests/test_agent_registry.py
  • tests/test_agent_registry_store.py
  • tinyagentos/agent_registry_store.py
  • tinyagentos/agent_token_auth.py
  • tinyagentos/config.py
  • tinyagentos/routes/agent_registry.py

Comment on lines +389 to +411
def renew_registry_token(
token: str,
public_key_pem: bytes,
private_key_pem: bytes,
*,
lifetime_seconds: int | None = None,
) -> str:
"""Renew an existing registry token.

Verifies the EdDSA signature (but does not require the token to be
unexpired) and returns a freshly-minted token with the same claims and
a new exp. This is the self-service path for agents whose token has
expired without human re-minting.
"""
payload = _verify_signature_only(token, public_key_pem)
return mint_registry_token(
payload["sub"],
private_key_pem,
user_id=payload.get("user_id", ""),
framework=payload.get("framework", ""),
project_id=payload.get("project_id"),
lifetime_seconds=lifetime_seconds,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against a payload missing the sub claim.

renew_registry_token accesses payload["sub"] directly at Line 405. _verify_signature_only only checks the signature, not the claim shape, so a validly-signed but malformed payload (missing sub) raises KeyError here instead of ValueError. The renewal route in tinyagentos/routes/agent_registry.py only catches ValueError, so this would surface as a 500 instead of a controlled 401. Other verification call sites (agent_token_auth.py) defensively use payload.get("sub", "") before use; apply the same pattern here.

🛡️ Proposed fix
     payload = _verify_signature_only(token, public_key_pem)
+    canonical_id = payload.get("sub")
+    if not canonical_id:
+        raise ValueError("token missing sub claim")
     return mint_registry_token(
-        payload["sub"],
+        canonical_id,
         private_key_pem,
         user_id=payload.get("user_id", ""),
         framework=payload.get("framework", ""),
         project_id=payload.get("project_id"),
         lifetime_seconds=lifetime_seconds,
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def renew_registry_token(
token: str,
public_key_pem: bytes,
private_key_pem: bytes,
*,
lifetime_seconds: int | None = None,
) -> str:
"""Renew an existing registry token.
Verifies the EdDSA signature (but does not require the token to be
unexpired) and returns a freshly-minted token with the same claims and
a new exp. This is the self-service path for agents whose token has
expired without human re-minting.
"""
payload = _verify_signature_only(token, public_key_pem)
return mint_registry_token(
payload["sub"],
private_key_pem,
user_id=payload.get("user_id", ""),
framework=payload.get("framework", ""),
project_id=payload.get("project_id"),
lifetime_seconds=lifetime_seconds,
)
def renew_registry_token(
token: str,
public_key_pem: bytes,
private_key_pem: bytes,
*,
lifetime_seconds: int | None = None,
) -> str:
"""Renew an existing registry token.
Verifies the EdDSA signature (but does not require the token to be
unexpired) and returns a freshly-minted token with the same claims and
a new exp. This is the self-service path for agents whose token has
expired without human re-minting.
"""
payload = _verify_signature_only(token, public_key_pem)
canonical_id = payload.get("sub")
if not canonical_id:
raise ValueError("token missing sub claim")
return mint_registry_token(
canonical_id,
private_key_pem,
user_id=payload.get("user_id", ""),
framework=payload.get("framework", ""),
project_id=payload.get("project_id"),
lifetime_seconds=lifetime_seconds,
)
🤖 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 389 - 411, Update
renew_registry_token to retrieve the subject claim with payload.get("sub", "")
instead of direct payload["sub"] access, matching the defensive pattern used by
other verification call sites and ensuring malformed signed payloads raise
ValueError through mint_registry_token rather than KeyError.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file and relevant symbols"
if [ -f tinyagentos/agent_registry_store.py ]; then
  wc -l tinyagentos/agent_registry_store.py
  ast-grep outline tinyagentos/agent_registry_store.py --match renew_registry_token --view expanded || true
  echo "--- relevant lines 240-430 ---"
  sed -n '240,430p' tinyagentos/agent_registry_store.py | nl -ba -v240
else
  fd -i 'agent_registry_store.py' .
fi

echo "--- search for token handling symbols ---"
rg -n "TOKEN_(LIFETIME|RENEW_|EXPIRED)?|lifetime_seconds|renew_registry_token|mint_registry_token|active|verify_signature|jti" tinyagentos

Repository: jaylfc/taOS

Length of output: 361


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- relevant lines 240-430 ---"
sed -n '240,430p' tinyagentos/agent_registry_store.py | sed 's/^/: /'

echo "--- search token-related symbols ---"
rg -n "TOKEN_(LIFETIME|RENEW_|EXPIRED)?|lifetime_seconds|renew_registry_token|mint_registry_token|active|verify_signature|jti" tinyagentos || true

echo "--- inspect registry state references around relevant functions ---"
rg -n "_verify_signature_only|mint_registry_token|renew_registry_token|def .*registry_token|lookup|active|update|delete|agent" tinyagentos/agent_registry_store.py | sed 's/^/: /'

Repository: jaylfc/taOS

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- renew_registry_token_route ---"
sed -n '748,774p' tinyagentos/routes/agent_registry.py | sed 's/^/: /'

echo "--- agent_token_auth relevant lines ---"
sed -n '1,140p' tinyagentos/agent_token_auth.py | sed 's/^/: /'

echo "--- deterministic behavior probe ---"
python3 - <<'PY'
import json
from base64 import urlsafe_b64encode, urlsafe_b64decode
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption

def b64url(data): return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def b64url_decode(s): return urlsafe_b64decode(s + b"=" * (4 - len(s) % 4))

private = Ed25519PrivateKey.generate()
public = private.public_key()
priv_pem = private.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
pub_pem = public.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)

def sign(payload):
    header = b64url(json.dumps({"alg":"EdDSA","typ":"JWT"}, separators=(",", ":")).encode())
    payload_b64 = b64url(json.dumps(payload).encode())
    sig = b64url(private.sign(f"{header}.{payload_b64}".encode()))
    return f"{header}.{payload_b64}.{sig}"

for days in [25, 36500]:
    token = sign({"sub":"agent-a1b2","iss":"taos-registry","iat":0,"exp":days*86400,"jti":"old","user_id":"u"})
    # Minimal implementation matching tinyagentos: signature checked, exp ignored, new exp iat+lifetime
    payload = json.loads(b64url_decode(token.split(".")[1]))
    new_token = sign({"sub":payload["sub"],"iss":"taos-registry","iat":1,\
        "exp":1+86400,"jti":"new","user_id":payload.get("user_id",""),**{k:payload.get(k) for k in ["framework","project_id"]}})
    print({
        "days": days,
        "old_exp": payload["exp"],
        "old_expired_at_renewal_time": False,
        "new_exp": json.loads(new_token.split(".")[1])["exp"],
        "renewal_same_id": payload["sub"] == json.loads(new_token.split(".")[1])["sub"]
    })

token = sign({"sub":"agent-a1b2","iss":"taos-registry","iat":100,"exp":200,"jti":"old"})
payload = json.loads(b64url_decode(token.split(".")[1]))
try:
    public.verify(b64url_decode(token.split(".")[2]), f"{token.split('.')[0]}.{token.split('.')[1]}".encode())
    print("signature_verifies_any_expired_payload", payload["sub"])
except Exception as exc:
    print("signature_verification_failed", type(exc).__name__, str(exc))
PY

Repository: jaylfc/taOS

Length of output: 7356


Bound self-service token renewal.

renew_registry_token only verifies the EdDSA signature and accepts any matching payload, even an expired one. Because renew_registry_token_route returns the fresh token and normal verification later requires the agent to be active, a captured active bearer token can rotate indefinitely while that agent record stays active. This conflicts with the DEFAULT_REGISTRY_TOKEN_LIFETIME intent that leaked bearer credentials should not be permanent. Add an enforcement rule, such as rejecting renewal past a maximum grace period after exp or invalidating the superseded jti/issuer key scope.

🤖 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 389 - 411, Bound
renew_registry_token so a signed token cannot be renewed indefinitely after
expiration: enforce a maximum grace period relative to its exp (using the
existing registry lifetime configuration where appropriate) and reject tokens
beyond that window before minting the replacement. Keep valid unexpired tokens
and recently expired tokens eligible, while preserving the existing claim reuse
and minting behavior.

Comment thread tinyagentos/config.py
Comment on lines +65 to +66
registry_token_lifetime_seconds: int = 86400
registry_token_migration_cutoff_ts: float | None = 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.

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -B2 -A15 'def save_config' tinyagentos/config.py
rg -n 'to_dict\(' tinyagentos/config.py

Repository: jaylfc/taOS

Length of output: 1247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== config.py outline relevant =="
ast-grep outline tinyagentos/config.py --match AppConfig --view expanded || true

echo "== AppConfig class region =="
sed -n '1,130p' tinyagentos/config.py

echo "== save/load/save_locked regions =="
sed -n '320,355p' tinyagentos/config.py

Repository: jaylfc/taOS

Length of output: 8506


Persist the new registry-token fields when saving config.

save_config() writes config.to_dict() directly, and AppConfig.to_dict() only emits memory_url for scalar optional settings, so custom values for registry_token_lifetime_seconds and registry_token_migration_cutoff_ts can be dropped on any save_config()/save_config_locked() call and will reload as defaults.

Add explicit entries for these fields in to_dict() with the same conditional/default-comparison style used for memory_url.

🤖 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/config.py` around lines 65 - 66, Update AppConfig.to_dict() to
explicitly serialize registry_token_lifetime_seconds and
registry_token_migration_cutoff_ts, using the same
conditional/default-comparison pattern as memory_url so custom values persist
through save_config() and save_config_locked() while defaults remain omitted.

Comment thread tinyagentos/config.py
Comment on lines +199 to +200
registry_token_lifetime_seconds=int(data.get("registry_token_lifetime_seconds", 86400)),
registry_token_migration_cutoff_ts=float(data["registry_token_migration_cutoff_ts"]) if "registry_token_migration_cutoff_ts" in data else 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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unguarded numeric coercion can crash config loading.

int(data.get("registry_token_lifetime_seconds", 86400)) and float(data["registry_token_migration_cutoff_ts"]) raise an uncaught ValueError/TypeError if the YAML value is malformed (e.g., a non-numeric string), crashing load_config instead of falling back to the default. Wrap these in a try/except that logs a warning and falls back to the default, matching the defensive style already used for other config fields.

🤖 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/config.py` around lines 199 - 200, Update the config loading
logic for registry_token_lifetime_seconds and registry_token_migration_cutoff_ts
to catch invalid numeric values, log a warning, and use their respective
defaults instead of allowing ValueError or TypeError to escape. Follow the
existing defensive parsing pattern in load_config and preserve None when the
migration cutoff key is absent.

Comment on lines +747 to +775
@router.post("/api/agents/registry/token/renew")
async def renew_registry_token_route(request: Request):
"""Renew the caller's registry token.

Accepts the current Bearer token (even if expired) and returns a new
token with the same claims and a fresh exp. The agent must still be
active in the registry.
"""
auth_header = request.headers.get("Authorization", "")
if not auth_header.lower().startswith("bearer "):
raise HTTPException(status_code=401, detail="missing bearer token")

raw_token = auth_header[7:].strip()
private_pem, public_pem = _get_keypair(request)

try:
new_token = renew_registry_token(raw_token, public_pem, private_pem)
except ValueError as exc:
raise HTTPException(status_code=401, detail=str(exc)) from exc

store = _get_store(request)
new_payload = verify_registry_token(new_token, public_pem)
record = await store.get(new_payload["sub"])
if record is None or record.get("status") != "active":
raise HTTPException(status_code=403, detail="agent is not active in the registry")

return {"token": new_token}


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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm registry_token_lifetime_seconds is unused elsewhere in the codebase.
rg -n 'registry_token_lifetime_seconds' --type=py
rg -n -B2 -A8 'mint_registry_token\(' tinyagentos/routes/agent_registry.py

Repository: jaylfc/taOS

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -u

echo "git files around candidate names:"
git ls-files | rg '(^|/)tinyagentos/(routes/agent_registry|agent_registry_store|config)\.py$' || true

echo
echo "search config key:"
rg -n 'registry_token_lifetime_seconds' . || true

echo
echo "search token renewal/minting functions and calls:"
rg -n 'def (renew|m)int_registry_token|renew_registry_token|mint_registry_token|DEFAULT_REGISTRY_TOKEN_LIFETIME|registry_token_lifetime' . || true

echo
echo "line counts for candidate files:"
git ls-files | rg '(^|/)tinyagentos/(routes/agent_registry|agent_registry_store|config)\.py$' | while read -r f; do wc -l "$f"; done

Repository: jaylfc/taOS

Length of output: 7062


🏁 Script executed:

#!/bin/bash
set -u

echo "agent_registry_store token functions:"
sed -n '250,420p' tinyagentos/agent_registry_store.py | cat -n

echo
echo "agent_registry registry route registration:"
sed -n '240,290p' tinyagentos/routes/agent_registry.py | cat -n

echo
echo "agent_registry registry route registration 2:"
sed -n '320,355p' tinyagentos/routes/agent_registry.py | cat -n

echo
echo "agent_registry renew route:"
sed -n '740,780p' tinyagentos/routes/agent_registry.py | cat -n

echo
echo "config field parsing:"
sed -n '55,72p' tinyagentos/config.py | cat -n
sed -n '188,205p' tinyagentos/config.py | cat -n

echo
echo "verify all route call sites pass no lifetime_seconds keyword:"
python3 - <<'PY'
import ast, pathlib
for path in [pathlib.Path('tinyagentos/routes/agent_registry.py')]:
    tree = ast.parse(path.read_text())
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name) and node.func.id in ('mint_registry_token','renew_registry_token'):
                kw = [kw.arg for kw in node.keywords]
                print(f'{path}:{node.lineno}: {node.func.id} keywords={kw}')
PY

Repository: jaylfc/taOS

Length of output: 15249


Use configured registry_token_lifetime_seconds when minting registry tokens.

mint_registry_token() and renew_registry_token() use DEFAULT_REGISTRY_TOKEN_LIFETIME only when lifetime_seconds is None, but both route call sites pass no value. Pass request.app.state.config.registry_token_lifetime_seconds into mint_registry_token() for /api/agents/registry/register and into renew_registry_token() for /api/agents/registry/token/renew so config.yaml lifetime settings apply.

🤖 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 747 - 775, Update the
registry token route call sites to pass
request.app.state.config.registry_token_lifetime_seconds as lifetime_seconds:
use it when minting in the /api/agents/registry/register handler and when
renewing in renew_registry_token_route. Preserve the existing token validation
and response behavior.

@jaylfc
jaylfc marked this pull request as draft August 2, 2026 14:34
@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

I have converted this to DRAFT so it cannot be merged by accident. As written, deploying it takes the entire fleet offline at once, and the agents cannot recover themselves. Please do not undraft until blockers 1 to 5 are done.

Credit where it is due first: the store-level crypto is correct and genuinely red-first. I mutated the expiry enforcement four different ways and each one went red, including deleting the exp block entirely (3 failures) and forcing the migration window permanently closed (1 failure). That is the part the card cared most about and you delivered it properly.

BLOCKING 1, the renewal path does not work for agents. POST /api/agents/registry/token/renew is not in _AGENT_TOKEN_PATHS in auth_middleware.py, so the middleware rejects it before the handler runs. Proven with a cookie-less bearer-only client against the real app: 401 Authentication required. Your tests pass because the registry_client fixture carries an ADMIN SESSION COOKIE, so they authenticate as the human admin, not as the agent. Renewal therefore requires exactly the manual human re-mint the card said must not be required. Add the route to the allowlist and add a test with a cookie-less client.

BLOCKING 2, the migration window defaults to ZERO. registry_token_migration_cutoff_ts defaults to None, which means allow_no_exp_until is None, which means every legacy token without exp is rejected on the first request after deploy. Every agent on the Pi, every internal driver identity, everything on the A2A bus goes to 401 simultaneously, and per blocker 1 they cannot renew out of it. Ship a non-None effective default (a computed now+30d at first start, or a documented mandatory upgrade step). Not None.

BLOCKING 3, the operator escape hatch is not durable. AppConfig.to_dict() does not emit either new field, and save_config writes yaml.dump(to_dict()). There are 11 save_config call sites, so an operator who sets a migration cutoff loses it the next time anything touches settings, mid-migration and silently. Add both fields to to_dict() plus a load-save-load round-trip test.

BLOCKING 4, renewal re-creates the eternal credential this PR exists to remove. renew_registry_token does a signature-only check with no time bound at all. Probed: a token that expired TEN YEARS ago renews into a fresh 24h token, and a legacy no-exp token renews regardless of the cutoff. So a leaked bearer is still permanent, the attacker just renews forever. Bound it: reject tokens expired beyond a configurable grace, and pass allow_no_exp_until through.

BLOCKING 5, this conflicts with hognek's PR 2208 in both senses. Semantically, 2208 revokes by rejecting tokens whose iat is older than token_min_iat; your renewal mints iat=now from a signature-only check that never consults it, so a rotated-out token can be laundered into a valid post-rotation one, defeating rotation entirely. It does not bite today only because renewal is unreachable, so fixing blocker 1 without this opens it. They must land together. Textually you also conflict in routes/agent_registry.py and tests/test_agent_registry_store.py.

SHOULD FIX: registry_token_lifetime_seconds is read by nothing, so the configurable lifetime is inert (probed: config set to 60, minted lifetime still 86400) - wire it into all five mint/renew sites or delete the field. Add clock-skew leeway (60s) to both comparisons; today exp+2s is rejected, which will cause spurious 401s across machines. Add HTTP-layer migration-window tests, because mutating the cutoff getter to infinity currently leaves 222 tests green. Guard the numeric coercions in load_config, since an ISO date string in a field named _ts crashes taOS at startup. Audit-log renewal the way minting already is. Refresh user_id/framework/project_id from the registry record rather than replaying the old token's claims. And three docstrings you edited still say the token carries no exp claim.

Also please revert the unrelated comment typo in tests/test_agent_registry_store.py where metacharacters became metachrs.

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-orB review

VERDICT: Code review complete - several correctness and test issues found.

  • tests/test_agent_registry_store.py:270 - _build_token_no_exp uses _b64url_encode which is not imported (private function in agent_registry_store.py). Tests will fail with NameError.
  • tinyagentos/agent_registry_store.py:280-310 - _verify_signature_only does not validate the JWT header alg field; a token with {"alg":"none"} would pass signature verification (though EdDSA verify would fail, explicit alg check is defense-in-depth).
  • tinyagentos/routes/agent_registry.py:744-778 - Renewal endpoint checks agent status after minting new token. If agent is deactivated, a token is minted then discarded (minor inefficiency, not a security issue).
  • tests/test_agent_registry.py:580-620 - Missing integration test for renewal with an expired token (the primary use case). Current test only uses a fresh token.
  • tinyagentos/agent_token_auth.py:103,185 - Migration cutoff defaults to None, causing immediate rejection of tokens without exp claim. This is a breaking change for existing deployments unless registry_token_migration_cutoff_ts is explicitly configured.
  • tinyagentos/config.py:196 - No validation that registry_token_migration_cutoff_ts is a reasonable future timestamp if provided.
  • tinyagentos/agent_registry_store.py:395 - verify_registry_token parameter allow_no_exp_until default None combined with agent_token_auth.py passing None means migration window is closed by default. Document this clearly or flip default to a far-future timestamp for smoother migration.

Automated first-pass review by the nemotron-ultra-orB lane. The lead still reviews before merge.

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: No blocking issues found.

Automated first-pass review by the nemotron-super lane. The lead still reviews before merge.

@jaylfc

jaylfc commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: No blocking issues found.

Automated first-pass review by the nemotron-super lane. The lead still reviews before merge.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant