Skip to content

tsk-x46zyb [OPEN] Observatory queue/throttle routes reject the agent - #2201

Closed
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-x46zyb
Closed

tsk-x46zyb [OPEN] Observatory queue/throttle routes reject the agent#2201
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-x46zyb

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Autonomous build of board card tsk-x46zyb.

Files:
tests/test_routes_observatory.py | 222 +++++++++++++++++++++++++++++++++-----
tinyagentos/auth_middleware.py | 11 +-
tinyagentos/routes/observatory.py | 106 +++++++++++++++---
3 files changed, 302 insertions(+), 37 deletions(-)

Summary by CodeRabbit

  • New Features

    • Added Observatory dispatch queue controls for viewing and setting queue limits.
    • Agents with Observatory control permissions can manage pause settings, throttling, and queue limits.
    • Authenticated agents can read Observatory pause, throttle, and queue status.
  • Bug Fixes

    • Improved reliability when multiple Observatory settings are updated concurrently.
    • Restricted control updates for agents without the required permissions.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Observatory pause and throttle routes now use scoped agent or admin authorization, and agent-token passthrough includes Observatory paths. A persisted queue-control API was added with global and per-lane limits. Tests now cover agent permissions, persistence, authenticated reads, and shared client helpers.

Changes

Observatory controls

Layer / File(s) Summary
Authorize pause and throttle controls
tinyagentos/auth_middleware.py, tinyagentos/routes/observatory.py, tests/test_routes_observatory.py
Observatory reads accept authenticated agents or users, while writes require an admin session or the observatory_control scope.
Add dispatch queue controls
tinyagentos/routes/observatory.py, tests/test_routes_observatory.py
Adds persisted GET/POST queue controls for global and per-lane limits, with scoped write authorization and coverage for success, denial, reads, and persistence.
Refactor authorization test setup
tests/test_routes_observatory.py
Adds reusable agent, non-admin, and project helpers and updates related authorization and fleet assertions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • jaylfc/taOS#1341: Both modify Observatory pause authorization and its tests.
  • jaylfc/taOS#1507: Both extend registry-JWT passthrough allowlisting in the authentication middleware.
  • jaylfc/taOS#1774: Both update agent-token path authorization in the middleware.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title points to the Observatory queue/throttle auth change and is relevant to the PR, even if it simplifies the behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-x46zyb

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Enable scoped agent access to Observatory controls

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Allows active agent JWTs to read pause, throttle, and queue controls.
• Restricts agent writes to observatory_control while preserving admin-session access.
• Adds persistent queue limits and end-to-end authorization coverage.
Diagram

sequenceDiagram
    actor A as Agent
    actor H as Human Admin
    participant M as Auth Middleware
    participant O as Observatory API
    participant T as Token Auth
    participant S as JSON State
    A->>M: Bearer request
    M->>O: Allowlisted route
    alt Read control
        O->>T: Verify active identity
    else Write control
        O->>T: Verify control scope
    end
    T-->>O: Agent authorized
    H->>M: Session request
    M->>O: Admin context
    O->>S: Read or atomic write
    S-->>O: Control state
    O-->>A: API response
    O-->>H: API response
Loading
High-Level Assessment

The approach matches the repository’s established least-privilege model: middleware narrowly allowlists agent-reachable paths while route dependencies perform identity and scope authorization. Reusing the existing registry-token checks and atomic JSON persistence is preferable to introducing a separate authentication or storage abstraction for three related controls.

Files changed (3) +302 / -37

Enhancement (1) +94 / -12
observatory.pyAuthorize agent controls and add persistent queue limits +94/-12

Authorize agent controls and add persistent queue limits

• Introduces shared Observatory read and write authorization dependencies: active agents may read, while writes require an admin session or 'observatory_control' grant. Adds global and per-lane queue-limit endpoints backed by atomically written JSON state, and applies agent-aware authorization to pause and throttle routes.

tinyagentos/routes/observatory.py

Bug fix (1) +10 / -1
auth_middleware.pyAllowlist agent JWT access to Observatory control routes +10/-1

Allowlist agent JWT access to Observatory control routes

• Adds pause, throttle, and queue endpoints to the exact registry-token passthrough allowlist. Authorization remains fail-closed because each route independently validates agent identity or the required control grant.

tinyagentos/auth_middleware.py

Tests (1) +198 / -24
test_routes_observatory.pyAdd agent authorization and queue-control integration coverage +198/-24

Add agent authorization and queue-control integration coverage

• Adds cookieless agent-token and real non-admin session helpers. Covers queue persistence, global and per-lane limits, active-agent reads, scoped writes, and rejection of unauthorized agents and non-admin users.

tests/test_routes_observatory.py

@gitar-bot

gitar-bot Bot commented Jul 28, 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

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (5)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. observatory_control is not grantable 📜 Skill insight ≡ Correctness
Description
The new Observatory write authorization requires observatory_control, but that scope is absent
from the closed vocabularies used by both consent/scope-request approval and internal-agent minting.
Production agents therefore cannot obtain the required permission through supported provisioning
flows, while regression tests conceal the issue by inserting the grant directly into the store.
Code

tinyagentos/routes/observatory.py[106]

+    cid = await check_agent_scope(request, "observatory_control")
Relevance

⭐⭐⭐ High

Required scope is unreachable through supported provisioning, directly defeating agent control.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2185251 requires every new scope to be present in both _ALLOWED_SCOPES and VALID_SCOPES,
whose validation branches reject scopes outside those lists. The Observatory route now checks
observatory_control, but both production allowlists omit it; the tests instead invoke
agent_grants.add_grant directly, bypassing validation and therefore failing to prove that an
operator can provision the required permission through a supported API.

tinyagentos/routes/observatory.py[104-112]
tinyagentos/routes/agent_registry.py[87-100]
tinyagentos/routes/agent_auth_requests.py[43-81]
tests/test_routes_observatory.py[10-33]
tinyagentos/routes/agent_auth_requests.py[1046-1057]
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
The Observatory write routes require an `observatory_control` grant that none of the supported production provisioning APIs currently recognize, preventing agents from acquiring the permission through normal minting or consent flows.

## Issue Context
Add `observatory_control` to both synchronized, authoritative grantable-scope vocabularies and any user-facing scope metadata. Retain API-level tests that request, approve, and mint the scope through a public provisioning flow rather than relying only on direct grant-store insertion.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[104-112]
- tinyagentos/routes/agent_registry.py[87-100]
- tinyagentos/routes/agent_auth_requests.py[43-81]
- tests/test_routes_observatory.py[10-33]
- tests/test_routes_observatory.py[157-180]

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


2. _read_queue performs synchronous I/O 📜 Skill insight ≡ Correctness
Description
_read_queue calls Path.exists() and Path.read_text() synchronously and is invoked directly by
async route handlers. These filesystem operations can block the event loop for concurrent requests.
Code

tinyagentos/routes/observatory.py[R220-223]

+    if not p.exists():
+        return {"global": None, "lanes": {}}
+    try:
+        data = json.loads(p.read_text())
Relevance

⭐⭐⭐ High

Team previously accepted moving blocking probes off async route event loops.

PR-#1542

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2185088 requires all route I/O to use await. The added helper performs synchronous existence
and content reads, and get_queue and set_queue call it without awaiting asynchronous filesystem
work.

tinyagentos/routes/observatory.py[218-231]
tinyagentos/routes/observatory.py[239-254]
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
The new async queue routes perform blocking filesystem reads through the synchronous `_read_queue` helper.

## Issue Context
Move the filesystem work to an asynchronous file API or `asyncio.to_thread`, make the helper awaitable, and update every queue-route caller.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[218-231]
- tinyagentos/routes/observatory.py[239-254]

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


3. set_queue writes synchronously 📜 Skill insight ≡ Correctness
Description
The async set_queue handler invokes _atomic_write directly, which performs directory creation,
temporary-file writes, replacement, and cleanup synchronously. A slow filesystem can block the
application event loop during queue updates.
Code

tinyagentos/routes/observatory.py[261]

+        _atomic_write(_queue_path(request), state)
Relevance

⭐⭐⭐ High

Synchronous filesystem writes inside an async handler match accepted blocking-I/O concerns.

PR-#1542
PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2185088 requires route filesystem operations to be awaited. Line 261 directly calls the
synchronous _atomic_write, whose implementation performs multiple blocking filesystem operations.

tinyagentos/routes/observatory.py[245-262]
tinyagentos/routes/observatory.py[61-76]
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
The new queue mutation route performs its atomic filesystem write synchronously inside an async handler.

## Issue Context
Run `_atomic_write` through an awaited asynchronous boundary such as `asyncio.to_thread`, while preserving the existing write lock and atomic replacement behavior.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[245-262]

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


View more (2)
4. _new_project accepts ambiguous statuses 📜 Skill insight ≡ Correctness
Description
The helper accepts either HTTP 200 or 201 even though project creation deterministically returns
200. This weakens the regression tests by allowing an unintended status-code change to pass.
Code

tests/test_routes_observatory.py[60]

+    assert resp.status_code in (200, 201), resp.text
Relevance

⭐⭐⭐ High

Small deterministic test-strengthening changes have accepted precedent.

PR-#449

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2233971 requires deterministic test values to use exact equality and verify resulting state.
_new_project uses membership in (200, 201) even though the project route returns its object with
FastAPI's default 200 response.

tests/test_routes_observatory.py[58-61]
tinyagentos/routes/projects.py[122-163]
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
The new project helper uses a multi-value status assertion instead of exact equality.

## Issue Context
The project creation route currently returns HTTP 200, so assert that exact value and retain the returned project identifier as the resulting-state check.

## Fix Focus Areas
- tests/test_routes_observatory.py[58-61]

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


5. Local token rejected 🐞 Bug ≡ Correctness
Description
_require_observatory_read validates every Bearer credential as a registry JWT before honoring
middleware-established authentication, so valid local admin tokens used by taosctl receive 401
instead of reading Observatory state. The write resolver has the same ordering, also breaking pause,
throttle, and queue updates made with the local token.
Code

tinyagentos/routes/observatory.py[96]

+    cid = await check_agent_identity(request)
Relevance

⭐⭐⭐ High

Direct authentication regression contradicts the PR’s stated intent and breaks taosctl.

PR-#1662

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The middleware recognizes the local Bearer token, sets admin state, and forwards the unchanged
header; the new resolver then passes that same header to a verifier that treats every Bearer token
as a registry JWT. Taosctl sends its configured administrative token through this exact header,
while the existing A2A resolver correctly gives middleware-established admin state precedence.

tinyagentos/auth_middleware.py[364-391]
tinyagentos/agent_token_auth.py[169-179]
tinyagentos/cli/taosctl/client.py[60-74]
tinyagentos/routes/a2a_bus.py[55-69]
tinyagentos/routes/observatory.py[94-112]

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

## Issue description
Observatory authentication attempts registry-JWT validation before honoring the admin identity already established by middleware. A valid local admin Bearer token is consequently parsed as a registry JWT and rejected.

## Issue Context
Check `request.state.is_admin` or the authenticated human identity before invoking agent-token validation, following the established A2A authorization pattern. Add coverage using a valid local admin token for both reads and writes.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[94-112]
- tinyagentos/routes/a2a_bus.py[55-69]
- tests/test_routes_observatory.py[64-192]

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



Remediation recommended

6. Malformed queue state crashes 🐞 Bug ☼ Reliability
Description
_read_queue assumes successfully decoded JSON and its lanes member are dictionaries, so valid
malformed state such as [] or {"lanes": []} raises AttributeError. Both queue GET and POST
then return 500 until the persisted file is manually repaired.
Code

tinyagentos/routes/observatory.py[227]

+    for k, v in (data.get("lanes") or {}).items():
Relevance

⭐⭐⭐ High

Concrete malformed persisted-state crash matches accepted input-hardening patterns.

PR-#248
PR-#419

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
After catching only I/O and parse failures, _read_queue calls data.get and then .items()
without validating either object's type. The approval-state reader in the same module explicitly
validates these exact persisted-JSON shapes before accessing them.

tinyagentos/routes/observatory.py[218-231]
tinyagentos/routes/observatory.py[289-313]

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 queue-state reader handles invalid JSON but not valid JSON with an unexpected top-level or nested shape.

## Issue Context
Fall back to the default state when the decoded value is not a dictionary, and treat a non-dictionary `lanes` value as an empty mapping. Add regression tests for scalar/list top-level state and non-mapping lanes.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[218-231]
- tinyagentos/routes/observatory.py[289-313]
- tests/test_routes_observatory.py[198-229]

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



Informational

7. Queue routes lack response models 📜 Skill insight ✧ Quality
Description
The new queue endpoints return raw dictionaries and declare no Pydantic response_model. Their
response shape is therefore neither schema-validated nor represented accurately in the generated API
contract.
Code

tinyagentos/routes/observatory.py[R239-246]

+@router.get("/api/observatory/queue")
+async def get_queue(request: Request, _auth: None = Depends(_require_observatory_read)):
+    """Current dispatch queue caps."""
+    return _read_queue(request)
+
+
+@router.post("/api/observatory/queue")
+async def set_queue(body: QueueBody, request: Request, _actor: str = Depends(_require_observatory_write)):
Relevance

⭐ Low

Near-identical response_model request for raw route dictionaries was explicitly rejected recently.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2185155 requires route request and response payloads to use Pydantic models. Although
QueueBody validates the POST body, both new decorators omit response_model and both handlers
return raw state dictionaries.

tinyagentos/routes/observatory.py[234-246]
tinyagentos/routes/observatory.py[252-262]
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
The new queue GET and POST routes return raw dictionaries without a declared Pydantic response schema.

## Issue Context
Define a queue-state response model, accounting for the `global` field name with an alias if needed, and attach it to both route decorators.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[234-246]
- tinyagentos/routes/observatory.py[252-262]

ⓘ 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


async def _require_observatory_write(request: Request) -> str:
from tinyagentos.agent_token_auth import check_agent_scope
cid = await check_agent_scope(request, "observatory_control")

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. observatory_control is not grantable 📜 Skill insight ≡ Correctness

The new Observatory write authorization requires observatory_control, but that scope is absent
from the closed vocabularies used by both consent/scope-request approval and internal-agent minting.
Production agents therefore cannot obtain the required permission through supported provisioning
flows, while regression tests conceal the issue by inserting the grant directly into the store.
Agent Prompt
## Issue description
The Observatory write routes require an `observatory_control` grant that none of the supported production provisioning APIs currently recognize, preventing agents from acquiring the permission through normal minting or consent flows.

## Issue Context
Add `observatory_control` to both synchronized, authoritative grantable-scope vocabularies and any user-facing scope metadata. Retain API-level tests that request, approve, and mint the scope through a public provisioning flow rather than relying only on direct grant-store insertion.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[104-112]
- tinyagentos/routes/agent_registry.py[87-100]
- tinyagentos/routes/agent_auth_requests.py[43-81]
- tests/test_routes_observatory.py[10-33]
- tests/test_routes_observatory.py[157-180]

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

Comment on lines +220 to +223
if not p.exists():
return {"global": None, "lanes": {}}
try:
data = json.loads(p.read_text())

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. _read_queue performs synchronous i/o 📜 Skill insight ≡ Correctness

_read_queue calls Path.exists() and Path.read_text() synchronously and is invoked directly by
async route handlers. These filesystem operations can block the event loop for concurrent requests.
Agent Prompt
## Issue description
The new async queue routes perform blocking filesystem reads through the synchronous `_read_queue` helper.

## Issue Context
Move the filesystem work to an asynchronous file API or `asyncio.to_thread`, make the helper awaitable, and update every queue-route caller.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[218-231]
- tinyagentos/routes/observatory.py[239-254]

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

state["lanes"][scope] = limit
else:
state["lanes"].pop(scope, None)
_atomic_write(_queue_path(request), state)

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

3. set_queue writes synchronously 📜 Skill insight ≡ Correctness

The async set_queue handler invokes _atomic_write directly, which performs directory creation,
temporary-file writes, replacement, and cleanup synchronously. A slow filesystem can block the
application event loop during queue updates.
Agent Prompt
## Issue description
The new queue mutation route performs its atomic filesystem write synchronously inside an async handler.

## Issue Context
Run `_atomic_write` through an awaited asynchronous boundary such as `asyncio.to_thread`, while preserving the existing write lock and atomic replacement behavior.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[245-262]

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


async def _new_project(client, name="alpha", slug="alpha"):
resp = await client.post("/api/projects", json={"name": name, "slug": slug})
assert resp.status_code in (200, 201), resp.text

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. _new_project accepts ambiguous statuses 📜 Skill insight ≡ Correctness

The helper accepts either HTTP 200 or 201 even though project creation deterministically returns
200. This weakens the regression tests by allowing an unintended status-code change to pass.
Agent Prompt
## Issue description
The new project helper uses a multi-value status assertion instead of exact equality.

## Issue Context
The project creation route currently returns HTTP 200, so assert that exact value and retain the returned project identifier as the resulting-state check.

## Fix Focus Areas
- tests/test_routes_observatory.py[58-61]

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


async def _require_observatory_read(request: Request) -> None:
from tinyagentos.agent_token_auth import check_agent_identity
cid = await check_agent_identity(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

6. Local token rejected 🐞 Bug ≡ Correctness

_require_observatory_read validates every Bearer credential as a registry JWT before honoring
middleware-established authentication, so valid local admin tokens used by taosctl receive 401
instead of reading Observatory state. The write resolver has the same ordering, also breaking pause,
throttle, and queue updates made with the local token.
Agent Prompt
## Issue description
Observatory authentication attempts registry-JWT validation before honoring the admin identity already established by middleware. A valid local admin Bearer token is consequently parsed as a registry JWT and rejected.

## Issue Context
Check `request.state.is_admin` or the authenticated human identity before invoking agent-token validation, following the established A2A authorization pattern. Add coverage using a valid local admin token for both reads and writes.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[94-112]
- tinyagentos/routes/a2a_bus.py[55-69]
- tests/test_routes_observatory.py[64-192]

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

except (OSError, ValueError):
return {"global": None, "lanes": {}}
lanes = {}
for k, v in (data.get("lanes") or {}).items():

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

7. Malformed queue state crashes 🐞 Bug ☼ Reliability

_read_queue assumes successfully decoded JSON and its lanes member are dictionaries, so valid
malformed state such as [] or {"lanes": []} raises AttributeError. Both queue GET and POST
then return 500 until the persisted file is manually repaired.
Agent Prompt
## Issue description
The queue-state reader handles invalid JSON but not valid JSON with an unexpected top-level or nested shape.

## Issue Context
Fall back to the default state when the decoded value is not a dictionary, and treat a non-dictionary `lanes` value as an empty mapping. Add regression tests for scalar/list top-level state and non-mapping lanes.

## Fix Focus Areas
- tinyagentos/routes/observatory.py[218-231]
- tinyagentos/routes/observatory.py[289-313]
- tests/test_routes_observatory.py[198-229]

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

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-orB review

VERDICT: Significant test coverage gap for pause endpoint agent authorization; potential test isolation issues with duplicate agent handles; minor auth return type inconsistency.

  • tests/test_routes_observatory.py:149-158 - Missing test test_agent_with_observatory_control_can_set_pause — throttle and queue have equivalent tests verifying agents with observatory_control scope can write, but pause does not. This leaves the pause write endpoint untested for agent JWT authorization.

  • tests/test_routes_observatory.py:17-18 - _mint_agent uses hardcoded handle "@taOS-dev" by default; multiple tests call it without overriding handle, risking duplicate registration collisions and test flakiness.

  • tests/test_routes_observatory.py:62-68 - _non_admin_client creates user via add_user_invite(username, "admin") — if "admin" role grants admin privileges, this test user would be admin and test_non_admin_cannot_pause would incorrectly pass (expects 403). Verify invite role semantics.

  • tinyagentos/routes/observatory.py:95-110 - _require_observatory_write returns cid (canonical_id) for agent tokens but uid (user_id) for admin sessions; inconsistent identifier types. Route handlers accept _actor: str but never use it — either use the value or drop the return.

  • tinyagentos/routes/observatory.py:88-93 - _require_observatory_read calls check_agent_identity but comment says "active agent token" — verify check_agent_identity actually checks agent status == "active"; if not, inactive agents could read observatory state.

  • tinyagentos/auth_middleware.py:38-44 - _AGENT_OBSERVATORY_PATHS allows agent tokens on /api/observatory/pause for both GET/POST; route-level auth correctly enforces read vs write, but middleware allowlist should be documented as "token passthrough only, route enforces scopes."

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

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-kilo review

VERDICT: Multiple correctness bugs, security gaps, and missing tests.

  • tests/test_routes_observatory.py:63_non_admin_client creates a user with role "admin" but the helper is named/used as a non-admin client. This breaks test_non_admin_cannot_pause, test_non_admin_cannot_throttle, test_non_admin_cannot_set_approval_mode, test_non_admin_cannot_set_queue — they all test with an admin user.

  • tests/test_routes_observatory.py:37_mint_agent hardcodes user_id="u" in mint_registry_token; should use the actual user context or agent's canonical ID.

  • tests/test_routes_observatory.py:57_new_project asserts status_code in (200, 201) but doesn't validate response shape before accessing resp.json()["id"]; could KeyError on error responses.

  • tinyagentos/routes/observatory.py:290-296_require_observatory_read falls back to request.state.user_id without verifying the request is authenticated (no request.state.authenticated check). An unauthenticated request with a spoofed user_id in state could bypass auth.

  • tinyagentos/routes/observatory.py:308-316_require_observatory_write same issue: reads request.state.user_id / request.state.is_admin without verifying request.state.authenticated.

  • tinyagentos/routes/observatory.py:340QueueBody.scope field name is misleading; it represents a queue scope ("global" or lane handle), not an OAuth scope. Rename to queue_scope or target.

  • tinyagentos/routes/observatory.py:355set_queue clears a lane cap when limit is None (which _coerce_limit returns for None, 0, or negative). Passing max_queue: 0 clears the cap rather than setting it to 0 — surprising semantics; document or change.

  • Missing test coverage: No tests for agent access to /api/observatory/pause (GET/POST) with observatory_control scope; no tests for project-scoped observatory_control grants; no tests for concurrent queue/throttle/pause writes (race conditions on file state).

  • tests/test_routes_observatory.py:107test_fleet_idle_agent_carries_its_framework renames variable mineidle in the diff but the test logic is unchanged; harmless but noisy diff.

  • tinyagentos/routes/observatory.py_coerce_limit, _write_lock, _queue_path, _read_queue, _atomic_write are referenced but not shown in diff; verify they exist and handle concurrent reads/writes safely (read path lacks locking).

  • Style: _require_observatory_write docstring says "Never session-only" but code accepts admin sessions — misleading comment.
    VERDICT: Multiple correctness bugs, security gaps, and missing tests.

  • tests/test_routes_observatory.py:63_non_admin_client creates a user with role "admin" but the helper is named/used as a non-admin client. This breaks test_non_admin_cannot_pause, test_non_admin_cannot_throttle, test_non_admin_cannot_set_approval_mode, test_non_admin_cannot_set_queue — they all test with an admin user.

  • tests/test_routes_observatory.py:37_mint_agent hardcodes user_id="u" in mint_registry_token; should use the actual user context or agent's canonical ID.

  • tests/test_routes_observatory.py:57_new_project asserts status_code in (200, 201) but doesn't validate response shape before accessing resp.json()["id"]; could KeyError on error responses.

  • tinyagentos/routes/observatory.py:290-296_require_observatory_read falls back to request.state.user_id without verifying the request is authenticated (no request.state.authenticated check). An unauthenticated request with a spoofed user_id in state could bypass auth.

  • tinyagentos/routes/observatory.py:308-316_require_observatory_write same issue: reads request.state.user_id / request.state.is_admin without verifying request.state.authenticated.

  • tinyagentos/routes/observatory.py:340QueueBody.scope field name is misleading; it represents a queue scope ("global" or lane handle), not an OAuth scope. Rename to queue_scope or target.

  • tinyagentos/routes/observatory.py:355set_queue clears a lane cap when limit is None (which _coerce_limit returns for None, 0, or negative). Passing max_queue: 0 clears the cap rather than setting it to 0 — surprising semantics; document or change.

  • Missing test coverage: No tests for agent access to /api/observatory/pause (GET/POST) with observatory_control scope; no tests for project-scoped observatory_control grants; no tests for concurrent queue/throttle/pause writes (race conditions on file state).

  • tests/test_routes_observatory.py:107test_fleet_idle_agent_carries_its_framework renames variable mineidle in the diff but the test logic is unchanged; harmless but noisy diff.

  • tinyagentos/routes/observatory.py_coerce_limit, _write_lock, _queue_path, _read_queue, _atomic_write are referenced but not shown in diff; verify they exist and handle concurrent reads/writes safely (read path lacks locking).

  • Style: _require_observatory_write docstring says "Never session-only" but code accepts admin sessions — misleading comment.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tinyagentos/routes/observatory.py (2)

104-139: 🔒 Security & Privacy | 🔵 Trivial

Consider logging the acting identity on privileged writes.

_require_observatory_write resolves and returns the caller's canonical_id/user_id, but set_pause/set_throttle/set_queue discard it (_actor). Since these endpoints steer the whole fleet (pause, concurrency caps, queue caps), recording who made the change (agent vs. admin, and which id) would help incident review/audit trails without much added code.

🤖 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/observatory.py` around lines 104 - 139, Add audit logging
for privileged observatory mutations by using the canonical actor returned from
_require_observatory_write in set_pause, set_throttle, and set_queue instead of
discarding it; record whether the caller is an agent or admin and include its
identifier alongside the change details, using the module’s existing logging
mechanism.

208-264: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Queue read/write duplicates throttle's logic almost verbatim.

_queue_path/_read_queue/set_queue are structurally identical to _throttle_path/_read_throttle/set_throttle (same "global vs. lane cap with _coerce_limit" shape, same lock/atomic-write dance). Since this pattern now exists twice, consider extracting a shared helper (e.g. _read_capped_state(path) / _apply_scope_limit(state, scope, limit)) parameterized by path and field name, used by both throttle and queue.

♻️ Sketch
def _read_capped_state(path: Path) -> dict:
    if not path.exists():
        return {"global": None, "lanes": {}}
    try:
        data = json.loads(path.read_text())
    except (OSError, ValueError):
        return {"global": None, "lanes": {}}
    if not isinstance(data, dict):
        return {"global": None, "lanes": {}}
    lanes = {}
    for k, v in (data.get("lanes") or {}).items():
        lim = _coerce_limit(v)
        if lim is not None:
            lanes[str(k)] = lim
    return {"global": _coerce_limit(data.get("global")), "lanes": lanes}

Both _read_throttle and _read_queue become one-line wrappers around this, and the scope == "global" / elif limit is not None / else pop block in set_throttle/set_queue can similarly be factored into a shared mutator.

🤖 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/observatory.py` around lines 208 - 264, Extract the
duplicated capped-state handling from `_read_throttle` and `_read_queue` into a
shared reader such as `_read_capped_state`, preserving invalid-data
normalization and `_coerce_limit` behavior. Also factor the shared global/lane
update logic used by `set_throttle` and `set_queue` into a mutator, then have
both endpoints retain their existing paths, locking, and atomic writes while
delegating state transformation to the helpers.
🤖 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/observatory.py`:
- Around line 214-231: Update _read_queue to validate that the parsed JSON value
is a dictionary before accessing data.get; return the documented empty queue
default for valid non-object JSON such as lists or null, while preserving the
existing lane and limit coercion behavior for dictionary data.

---

Nitpick comments:
In `@tinyagentos/routes/observatory.py`:
- Around line 104-139: Add audit logging for privileged observatory mutations by
using the canonical actor returned from _require_observatory_write in set_pause,
set_throttle, and set_queue instead of discarding it; record whether the caller
is an agent or admin and include its identifier alongside the change details,
using the module’s existing logging mechanism.
- Around line 208-264: Extract the duplicated capped-state handling from
`_read_throttle` and `_read_queue` into a shared reader such as
`_read_capped_state`, preserving invalid-data normalization and `_coerce_limit`
behavior. Also factor the shared global/lane update logic used by `set_throttle`
and `set_queue` into a mutator, then have both endpoints retain their existing
paths, locking, and atomic writes while delegating state transformation to the
helpers.
🪄 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: 467b868a-ef4e-4db3-b10d-ac28ccd63843

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6dcaa and ea8792a.

📒 Files selected for processing (3)
  • tests/test_routes_observatory.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/routes/observatory.py

Comment on lines +214 to +231
def _queue_path(request: Request) -> Path:
return Path(request.app.state.data_dir) / "observatory_queue.json"


def _read_queue(request: Request) -> dict:
p = _queue_path(request)
if not p.exists():
return {"global": None, "lanes": {}}
try:
data = json.loads(p.read_text())
except (OSError, ValueError):
return {"global": None, "lanes": {}}
lanes = {}
for k, v in (data.get("lanes") or {}).items():
lim = _coerce_limit(v)
if lim is not None:
lanes[str(k)] = lim
return {"global": _coerce_limit(data.get("global")), "lanes": lanes}

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

Malformed queue file crashes instead of falling back.

_read_queue catches (OSError, ValueError) around json.loads, but if the persisted file contains valid JSON that isn't a dict (e.g. a hand-edited [] or null), data.get(...) raises AttributeError, which is uncaught and will 500 the request instead of degrading to the documented default {"global": None, "lanes": {}}. This mirrors the same latent gap already present in _read_throttle/_read_state.

🛡️ Proposed fix
     try:
         data = json.loads(p.read_text())
-    except (OSError, ValueError):
+    except (OSError, ValueError):
+        return {"global": None, "lanes": {}}
+    if not isinstance(data, dict):
         return {"global": None, "lanes": {}}
📝 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 _queue_path(request: Request) -> Path:
return Path(request.app.state.data_dir) / "observatory_queue.json"
def _read_queue(request: Request) -> dict:
p = _queue_path(request)
if not p.exists():
return {"global": None, "lanes": {}}
try:
data = json.loads(p.read_text())
except (OSError, ValueError):
return {"global": None, "lanes": {}}
lanes = {}
for k, v in (data.get("lanes") or {}).items():
lim = _coerce_limit(v)
if lim is not None:
lanes[str(k)] = lim
return {"global": _coerce_limit(data.get("global")), "lanes": lanes}
def _queue_path(request: Request) -> Path:
return Path(request.app.state.data_dir) / "observatory_queue.json"
def _read_queue(request: Request) -> dict:
p = _queue_path(request)
if not p.exists():
return {"global": None, "lanes": {}}
try:
data = json.loads(p.read_text())
except (OSError, ValueError):
return {"global": None, "lanes": {}}
if not isinstance(data, dict):
return {"global": None, "lanes": {}}
lanes = {}
for k, v in (data.get("lanes") or {}).items():
lim = _coerce_limit(v)
if lim is not None:
lanes[str(k)] = lim
return {"global": _coerce_limit(data.get("global")), "lanes": lanes}
🤖 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/observatory.py` around lines 214 - 231, Update _read_queue
to validate that the parsed JSON value is a dictionary before accessing
data.get; return the documented empty queue default for valid non-object JSON
such as lists or null, while preserving the existing lane and limit coercion
behavior for dictionary data.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Closing and recutting the card rather than leaving this open: the lane cannot push again to its own branch, and the fixes are specific enough to spec precisely. No criticism of the work, the shape is right and the negative tests are genuinely load-bearing (I sabotaged the write check and 5 tests went red, so the refusal path is real).

BLOCKER 1, the write path is dead in production. observatory.py:106 requires the scope observatory_control, but it is absent from BOTH VALID_SCOPES (agent_auth_requests.py:45-81) and _ALLOWED_SCOPES (agent_registry.py:90-102). Probed against the real provisioning routes: consent-request returns 400 unknown scopes, mint-internal returns 422. So no agent can ever hold the grant and POST /api/observatory/{pause,throttle,queue} stays exactly as unreachable as before. CI passed anyway because the test helper calls grants.add_grant directly and bypasses every validation layer, which is why the suite proves nothing about grantability. This is the third time today a green suite has certified a feature nobody can reach.

BLOCKER 2, a regression that breaks taosctl. observatory.py:94-112 runs the registry-JWT verifier BEFORE honouring the admin identity the middleware already established, so a local admin token now 401s. Proven head against dev on the same probe: GET and POST /api/observatory/pause and GET /throttle all 200 on dev and 401 here, with an untouched control route still 200 both sides. cli/taosctl/client.py:70 sends the local token as Bearer, so taosctl observatory pause/unpause/throttle/status is fully broken, as is any local-token poller. The correct pattern is already in this repo at a2a_bus.py:55-69: check request.state.is_admin FIRST, then fall through to the scope check.

Also unresolved, folded into the new card: the write scope is checked globally rather than per project, so a project-bound grant silently confers fleet-wide pause; the read gate uses check_agent_identity, which is any registered agent with zero grants rather than the project-scoped agent the card asked for, and it discloses the full lane roster; POST /api/observatory/pause has no agent-token test at all; docs/agent-coordination.md was not swept.

Adjudicated so nobody chases them: nemotron's claim that _non_admin_client creates an admin is WRONG (auth.py:357 hardcodes is_admin False, and the sabotage run proves those tests are live). The sync-I/O and non-dict-JSON findings are real but PRE-EXISTING, copied from _read_throttle; they do not belong in this card.

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