Skip to content

tsk-mtds32 [OPEN] S4e: device pair-requests + grant Decision (consen - #2233

Open
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-mtds32
Open

tsk-mtds32 [OPEN] S4e: device pair-requests + grant Decision (consen#2233
jaylfc wants to merge 1 commit into
devfrom
exec/tsk-mtds32

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Autonomous build of board card tsk-mtds32.

REVIEW WARNING (automated): this card's text asks for tests, but the diff changes no test file. Either the acceptance criteria are unmet or the card needs correcting. Do not merge without resolving this.

Files:
docs/design/postgres-bounded-adoption.md | 143 ----------------
tinyagentos/device_pair_requests_store.py | 253 +++++++++++++++++++++++++++++
tinyagentos/routes/device_pair_requests.py | 218 +++++++++++++++++++++++++
3 files changed, 471 insertions(+), 143 deletions(-)


Summary by Gitar

  • Device pairing routes:
    • Added API endpoints for creating and polling device pairing requests in tinyagentos/routes/device_pair_requests.py
  • Pairing request store:
    • Implemented DevicePairRequestsStore for persistent tracking and state management in tinyagentos/device_pair_requests_store.py

This will update automatically on new commits.

Summary by CodeRabbit

  • New Features
    • Added device-pairing requests with platform and display-name details.
    • Added six-digit verification codes for human confirmation.
    • Added request status polling, including pending, accepted, denied, and expired states.
    • Added administrator decision handling with one-time access tokens upon approval.
    • Added safeguards for pending-request limits, expiration, and duplicate decisions.
    • Added clear responses for missing or unavailable pairing requests.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds SQLite-backed device-pairing request storage and unauthenticated routes. The flow supports validation, expiry, admin decisions, capability-based polling, sanitized device data, and one-time scoped tokens.

Changes

Device pairing

Layer / File(s) Summary
Request store and lifecycle
tinyagentos/device_pair_requests_store.py
Defines the request schema, lifecycle constants, ten-minute expiry, safe read projection, creation, pending counts, expiry resolution, and oldest-first listing.
Decisions and token claims
tinyagentos/device_pair_requests_store.py
Adds conditional accepted, denied, and expired updates. Adds accepted-request device association and atomic one-time token claims.
Pairing request routes
tinyagentos/routes/device_pair_requests.py
Adds platform validation, pending-cap enforcement, verification-code generation, admin decision creation, notification, capability-based polling, sanitized device data, and scoped-token responses.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PairingRoutes
  participant DevicePairRequestsStore
  participant DecisionService
  participant DeviceRegistry
  Client->>PairingRoutes: Create pairing request
  PairingRoutes->>DevicePairRequestsStore: Persist request
  PairingRoutes->>DecisionService: Create admin decision
  PairingRoutes-->>Client: Return request ID and verification code
  Client->>PairingRoutes: Poll request ID
  PairingRoutes->>DevicePairRequestsStore: Read request and claim token
  PairingRoutes->>DeviceRegistry: Retrieve accepted device data
  PairingRoutes-->>Client: Return status, device data, and scoped token
Loading

Possibly related PRs

  • jaylfc/taOS#1674: Adds related device registry, scoped-token authentication, and Apple client device foundations.
🚥 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 identifies device pair requests and granting a Decision, which match the primary changes in the pull request.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 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-mtds32

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add device pairing requests store + Decisions-gated poll API

✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add SQLite store to persist device pair requests with TTL and atomic approve/deny.
• Add unauthenticated create/poll endpoints and raise a blocking Decision for admin consent.
• Release device scoped_token exactly once after approval and cap total pending requests.
Diagram

graph TD
  ext([External device/app]) --> prApi["Pair-requests API"] --> prStore["PairRequestsStore"] --> prDB[("pair_requests DB")]
  prApi --> decisionStore["DecisionStore"] --> admin(["Admin inbox/UI"])
  prApi --> notifs["Notifications"] --> admin
  admin --> decisionsApi["Decisions API"] --> prStore
  decisionsApi --> deviceStore["DeviceStore"] --> devicesDB[("devices DB")]
  prApi --> deviceStore
  subgraph Legend
    direction LR
    _actor(["Actor"]) ~~~ _svc["Service/Module"] ~~~ _db[("SQLite DB")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse existing auth-request store/route pattern
  • ➕ Less new schema/code by extending a proven consent-loop implementation
  • ➕ Potentially inherits existing tests and operational behavior
  • ➖ Device pairing has distinct semantics (device minting, one-time token claim) that may not fit cleanly
  • ➖ Overloads an agent-auth concept with device concerns
2. Ephemeral in-memory pending requests (no DB)
  • ➕ Simpler implementation; no schema/migrations
  • ➕ Automatically purges on restart
  • ➖ Breaks polling reliability across restarts
  • ➖ Harder to make decision transitions and one-time token claim robust/atomic
3. Stateless signed pair_request_id (JWT-like)
  • ➕ Avoids DB table for pending requests
  • ➕ Easy expiry via token claims
  • ➖ Still needs server-side state for one-time scoped_token release and double-mint prevention
  • ➖ Adds key-management/crypto surface area

Recommendation: The dedicated SQLite store + Decisions-based approval is the best fit for atomic transitions, audit trail, and one-time token handoff. Before merge, reviewers should confirm the missing integration points are addressed elsewhere (router registration and app.state.device_pair_requests initialization), and add tests for expiry-at-approve, atomic set_decision race safety, and scoped_token one-time release—this PR adds no tests despite security-sensitive behavior.

Files changed (2) +471 / -0

Enhancement (2) +471 / -0
device_pair_requests_store.pyAdd SQLite store for device pairing requests with TTL + atomic decision transitions +253/-0

Add SQLite store for device pairing requests with TTL + atomic decision transitions

• Creates a new device_pair_requests table and helper functions to compute live status/expiry. Implements create() with TTL, set_decision() using a conditional UPDATE to avoid approve/deny races, and claim_scoped_token() to enforce a one-time token release. All read paths intentionally exclude verify_code to prevent leakage beyond the creation response.

tinyagentos/device_pair_requests_store.py

device_pair_requests.pyAdd create/poll API endpoints for device pairing consent via Decisions +218/-0

Add create/poll API endpoints for device pairing consent via Decisions

• Adds POST /api/devices/pair-requests to accept unauthenticated requests, enforce a total pending cap, generate a 6-digit verify_code, and raise a blocking Decision plus best-effort notification to the instance admin. Adds GET /api/devices/pair-requests/{id} to poll status and, once accepted, return the device record and release scoped_token exactly once using token_claimed. Enforces a platform whitelist at the route layer.

tinyagentos/routes/device_pair_requests.py

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

🧹 Nitpick comments (7)
tinyagentos/routes/device_pair_requests.py (3)

180-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the swallowed notification failure.

The comment at lines 167-168 states that the bell notification is best-effort, so swallowing the exception is intentional. Discarding it without a log leaves no trace when the notification subsystem fails. Ruff reports this as S110 and BLE001.

♻️ Proposed change
-            except Exception:
-                pass
+            except Exception:
+                logger.warning(
+                    "device_pair_requests: bell notification failed", exc_info=True
+                )
🤖 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/device_pair_requests.py` around lines 180 - 181, Update
the notification exception handler in the device-pair request flow to retain
best-effort behavior while logging the swallowed exception. Replace the bare
pass in the visible except block with the project’s established logging
mechanism, preserving the existing broad exception handling and avoiding
propagation.

Source: Linters/SAST tools


48-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused constant and the unused request model.

_MAX_DEVICES_PER_USER is never referenced in this file. The docstring at line 23 states that DeviceStore.register owns the per-user cap, so this copy can drift from the real limit.

PairRequestIn is not used by either route. It also declares a verify_code field, which contradicts the security note at lines 19-20 stating that no endpoint accepts verify_code as input. A future author may wire it up and break that property.

♻️ Proposed change
 _VALID_PLATFORMS = frozenset({"ios", "watchos", "android"})
 _VERIFY_CODE_DIGITS = 6
-_MAX_DEVICES_PER_USER = 50
 
 
 class CreatePairRequest(BaseModel):
     platform: str
     display_name: str = ""
-
-
-class PairRequestIn(BaseModel):
-    verify_code: str | None = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/device_pair_requests.py` around lines 48 - 57, Remove the
unused _MAX_DEVICES_PER_USER constant and the unused PairRequestIn model from
this module. Leave DeviceStore.register as the sole owner of the per-user device
limit and ensure no request model exposes a verify_code input.

98-99: 📐 Maintainability & Code Quality | 🔵 Trivial

Tests are missing for this unauthenticated flow.

The PR description states that the card requests tests and that this must be resolved before merging. No test file is changed in this PR. This flow is unauthenticated and issues device credentials, so it needs coverage for the platform whitelist rejection, the pending cap 429, the 404 on an unknown id, the expired-pending status, and the single release of scoped_token across two polls.

Do you want me to generate the test module, or open an issue to track it?

🤖 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/device_pair_requests.py` around lines 98 - 99, Add tests
for the unauthenticated create_pair_request flow covering platform whitelist
rejection, the pending-cap 429 response, unknown-id 404 handling,
expired-pending status, and exactly one scoped_token release across two polls.
Place the coverage in a test module for the device pairing routes and exercise
the /api/devices/pair-requests endpoint without authentication.
tinyagentos/device_pair_requests_store.py (4)

29-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Promote the names that other modules import.

tinyagentos/routes/device_pair_requests.py imports _PENDING_CAP and _live_status from this module. Both names use the underscore prefix, which signals module-private use. Rename them to PENDING_CAP and live_status (or re-export public aliases) so the cross-module contract is explicit.

Also applies to: 87-93

🤖 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/device_pair_requests_store.py` around lines 29 - 32, Promote the
cross-module symbols in device_pair_requests_store by exposing public names
PENDING_CAP and live_status instead of relying on _PENDING_CAP and _live_status.
Update all definitions and references, including imports in
routes/device_pair_requests.py, while preserving the existing cap and
live-status behavior; public aliases are acceptable if compatibility with
internal references is needed.

220-234: 🔒 Security & Privacy | 🔵 Trivial

Consider a purge path for expired pending requests.

count_pending and list_pending both exclude rows past expires_at_ts. Those rows are never deleted or transitioned, so the table grows without bound and retains requester_ip and verify_code indefinitely. Add a periodic delete or a transition-to-expired sweep with a retention window. This also limits PII retention for requester_ip.

🤖 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/device_pair_requests_store.py` around lines 220 - 234, Add an
expiration sweep to DevicePairRequestsStore, invoked from the pending-request
flow such as count_pending and list_pending, that deletes expired pending rows
or transitions them to expired and removes them after a defined retention
window. Ensure the sweep uses expires_at_ts and preserves counting/listing only
live pending requests while preventing indefinite retention of requester_ip and
verify_code.

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

Silence the SQL-injection lint findings with a justification.

_SAFE_COLS is a module-level literal, so the f-string cannot carry caller input. Ruff (S608) and OpenGrep both report this line, which can fail the lint stage. Add a scoped suppression and state why the interpolation is safe.

♻️ Proposed change
         cur = await self._db.execute(
-            f"SELECT {_SAFE_COLS} FROM device_pair_requests WHERE id = ?",
+            # _SAFE_COLS is a module-level column-name literal, never caller input.
+            f"SELECT {_SAFE_COLS} FROM device_pair_requests WHERE id = ?",  # noqa: S608
             (pair_request_id,),
         )
🤖 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/device_pair_requests_store.py` around lines 208 - 211, Add a
narrowly scoped suppression for Ruff S608 and the OpenGrep SQL-injection finding
on the SELECT statement in the device-pair request lookup, with an adjacent
justification that `_SAFE_COLS` is a module-level literal containing no
caller-controlled input. Keep parameterization of `pair_request_id` unchanged.

Source: Linters/SAST tools


241-252: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use _SAFE_COLS instead of SELECT * in list_pending.

SELECT * reads verify_code into memory, and correctness then depends on the pop at line 251. get already projects _SAFE_COLS in SQL. Apply the same projection here so the nonce never leaves the database on this path.

♻️ Proposed change
         cur = await self._db.execute(
-            "SELECT * FROM device_pair_requests "
+            f"SELECT {_SAFE_COLS} FROM device_pair_requests "  # noqa: S608
             "WHERE status = 'pending' AND expires_at_ts > ? "
             "ORDER BY created_ts",
             (now_iso,),
         )
         rows = await cur.fetchall()
         out = []
         for row in rows:
             d = {k: row[k] for k in row.keys()}
-            d.pop("verify_code", None)
             out.append(d)
🤖 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/device_pair_requests_store.py` around lines 241 - 252, Update the
SQL query in list_pending to select the existing _SAFE_COLS projection instead
of SELECT *. Keep the pending-status, expiration, ordering, and result
conversion behavior unchanged; the query must exclude verify_code before rows
are fetched, and the post-fetch pop can be removed if no longer needed.
🤖 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/device_pair_requests_store.py`:
- Around line 1-19: Restore the module docstrings by placing each documentation
string before the future import. In tinyagentos/device_pair_requests_store.py
(lines 1-19), move the docstring above from __future__ import annotations; apply
the same ordering change in tinyagentos/routes/device_pair_requests.py (lines
1-28), with no other changes.
- Around line 161-177: Update set_decision’s pending-row UPDATE to allow
acceptance only when expires_at_ts has not passed, preserving atomicity and
returning None for expired requests. Ensure expired decisions cannot mint
device_id, and revise the method’s docstring comment to describe this expired
decision behavior.

In `@tinyagentos/routes/device_pair_requests.py`:
- Around line 88-95: The pairing route currently returns an unapprovable request
when no admin exists or Decision creation fails. In
tinyagentos/routes/device_pair_requests.py lines 88-95, update _admin_user_id to
log a warning when no admin is found and make create_pair_request return HTTP
503 instead of a request ID; at lines 164-165, raise
HTTPException(status_code=503, detail="pairing unavailable") immediately after
logging the Decision failure.
- Around line 88-95: Update _admin_user_id to select the primary admin
deterministically using an explicit rule such as the configured primary-admin ID
or earliest created_at, and handle the no-admin case explicitly. Ensure the
pairing route does not return a successful pair_request_id while silently
skipping approval: log a warning and/or reject the request with HTTP 503 before
creating the request when no admin resolves.
- Around line 113-136: Move the pending-cap enforcement from the separate
count/create flow in the route to an atomic store operation: update the store
method used by the pairing endpoint to perform a guarded INSERT that only
succeeds when pending rows are below _PENDING_CAP, returning the created record
or None. Update the route around store.create to map a None result to the
existing 429 response, while preserving the current success response and request
fields.
- Around line 81-85: Update _requester_ip to avoid returning the raw
request.client.host: when the immediate client belongs to a configured trusted
proxy range, extract the original client IP from the appropriate forwarded
header; otherwise use the direct peer address. Preserve the empty result when no
client is present and ensure forwarded headers are ignored for untrusted
clients.
- Around line 51-53: Update CreatePairRequest to enforce a maximum length for
display_name and constrain platform using a Literal derived from the existing
_VALID_PLATFORMS source. Then remove the redundant platform validation in the
route, or retain it only if the model constraint remains the single
authoritative definition.

---

Nitpick comments:
In `@tinyagentos/device_pair_requests_store.py`:
- Around line 29-32: Promote the cross-module symbols in
device_pair_requests_store by exposing public names PENDING_CAP and live_status
instead of relying on _PENDING_CAP and _live_status. Update all definitions and
references, including imports in routes/device_pair_requests.py, while
preserving the existing cap and live-status behavior; public aliases are
acceptable if compatibility with internal references is needed.
- Around line 220-234: Add an expiration sweep to DevicePairRequestsStore,
invoked from the pending-request flow such as count_pending and list_pending,
that deletes expired pending rows or transitions them to expired and removes
them after a defined retention window. Ensure the sweep uses expires_at_ts and
preserves counting/listing only live pending requests while preventing
indefinite retention of requester_ip and verify_code.
- Around line 208-211: Add a narrowly scoped suppression for Ruff S608 and the
OpenGrep SQL-injection finding on the SELECT statement in the device-pair
request lookup, with an adjacent justification that `_SAFE_COLS` is a
module-level literal containing no caller-controlled input. Keep
parameterization of `pair_request_id` unchanged.
- Around line 241-252: Update the SQL query in list_pending to select the
existing _SAFE_COLS projection instead of SELECT *. Keep the pending-status,
expiration, ordering, and result conversion behavior unchanged; the query must
exclude verify_code before rows are fetched, and the post-fetch pop can be
removed if no longer needed.

In `@tinyagentos/routes/device_pair_requests.py`:
- Around line 180-181: Update the notification exception handler in the
device-pair request flow to retain best-effort behavior while logging the
swallowed exception. Replace the bare pass in the visible except block with the
project’s established logging mechanism, preserving the existing broad exception
handling and avoiding propagation.
- Around line 48-57: Remove the unused _MAX_DEVICES_PER_USER constant and the
unused PairRequestIn model from this module. Leave DeviceStore.register as the
sole owner of the per-user device limit and ensure no request model exposes a
verify_code input.
- Around line 98-99: Add tests for the unauthenticated create_pair_request flow
covering platform whitelist rejection, the pending-cap 429 response, unknown-id
404 handling, expired-pending status, and exactly one scoped_token release
across two polls. Place the coverage in a test module for the device pairing
routes and exercise the /api/devices/pair-requests endpoint without
authentication.
🪄 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: 61062f39-71c7-4950-b8ca-c03f2e3e94a6

📥 Commits

Reviewing files that changed from the base of the PR and between c4964b1 and d835e0b.

📒 Files selected for processing (2)
  • tinyagentos/device_pair_requests_store.py
  • tinyagentos/routes/device_pair_requests.py

Comment on lines +1 to +19
from __future__ import annotations

"""Store for device pairing (consent) requests.

Each record tracks one inbound pairing request from an external device/app.
Pending requests wait for the instance user to approve or deny (surfaced
through a device_pairing Decision). Accepted requests carry the minted
``device_id`` so the caller can poll and retrieve the issued device token.

The state machine is: pending -> accepted | denied | expired (terminal).
``set_decision`` is atomic -- it uses a conditional UPDATE that only matches
rows still in ``pending`` status, so two concurrent approve/deny calls cannot
both win a read-check-then-write race.

The ``verify_code`` is a human-comparison nonce (F3): it is persisted only so
the Decision text can display it for the approving user, and is NEVER returned
by ``get``/poll -- the route layer strips it. It is never server-checked and no
endpoint accepts it as input.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Both new modules lose their docstring to the from __future__ import. Python assigns __doc__ only when the string literal is the first statement in the module. In both files from __future__ import annotations comes first, so the documentation block becomes a discarded expression.

  • tinyagentos/device_pair_requests_store.py#L1-L19: move the docstring at lines 3-19 above line 1, and place from __future__ import annotations directly after it.
  • tinyagentos/routes/device_pair_requests.py#L1-L28: move the docstring at lines 3-28 above line 1, and place from __future__ import annotations directly after it.
📍 Affects 2 files
  • tinyagentos/device_pair_requests_store.py#L1-L19 (this comment)
  • tinyagentos/routes/device_pair_requests.py#L1-L28
🤖 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/device_pair_requests_store.py` around lines 1 - 19, Restore the
module docstrings by placing each documentation string before the future import.
In tinyagentos/device_pair_requests_store.py (lines 1-19), move the docstring
above from __future__ import annotations; apply the same ordering change in
tinyagentos/routes/device_pair_requests.py (lines 1-28), with no other changes.

Comment on lines +161 to +177
now_iso = _iso(_now())
cur = await self._db.execute(
"""
UPDATE device_pair_requests
SET status = ?,
device_id = ?,
decided_ts = ?,
decided_by = ?
WHERE id = ? AND status = 'pending'
""",
(status, device_id if status == "accepted" else None,
now_iso, decided_by, pair_request_id),
)
await self._db.commit()
if cur.rowcount == 0:
return None
return await self.get(pair_request_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the device pairing grant path for an expiry check.
set -euo pipefail

fd -t f 'decisions.py' -x rg -n -C 12 '_apply_device_pairing_grant|device_pairing' {}

# Any other caller of set_decision / _is_expired across the repo.
rg -nP -C 6 '\bset_decision\s*\(|\b_is_expired\s*\('

Repository: jaylfc/taOS

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Candidate files:\n'
git ls-files | rg '(^|/)device_pair_requests_store\.py$|(^|/)decisions\.py$|(^|/)device_pair_requests\.py$|(^|/)routes/'

printf '\nFind matching source filenames:\n'
fd -t f 'device_pair_requests_store.py|decisions.py|device_pair_requests.py' || true

printf '\nStore file outline:\n'
ast-grep outline tinyagentos/device_pair_requests_store.py || true

printf '\nRelevant store sections:\n'
sed -n '1,220p' tinyagentos/device_pair_requests_store.py | cat -n

printf '\nSearch for grant/request routes and decision handlers:\n'
rg -n -C 8 'device_pair|pair_request|_apply_device_pairing_grant|set_decision|_is_expired|decides|pairing' . || true

Repository: jaylfc/taOS

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Store outline:\n'
ast-grep outline tinyagentos/device_pair_requests_store.py || true

printf '\nRelevant store file sections:\n'
sed -n '1,220p' tinyagentos/device_pair_requests_store.py | cat -n

printf '\nRoute decision handler references:\n'
rg -n -C 10 '_apply_device_pairing_grant|def .*device.*pair|kind.*device_pairing|pair_request_id' tinyagentos/routes/decisions.py || true

printf '\nAll small focused callers in tinyagentos/tests:\n'
rg -n -C 8 '\.set_decision|DevicePairRequestsStore|device_pair|pair_request_id' tinyagentos/tests tinyagentos/routes tinyagentos || true

printf '\nSQLite UPDATE behavior probe:\n'
python3 - <<'PY'
import sqlite3

con = sqlite3.connect(":memory:")
con.executescript("""
  CREATE TABLE device_pair_requests (
    id TEXT PRIMARY KEY,
    status TEXT,
    device_id TEXT,
    decided_ts TEXT,
    decided_by TEXT,
    expires_at_ts TEXT,
    claim_request TEXT,
    created_ts TEXT
  );
""")
con.execute("INSERT INTO device_pair_requests VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
    ["a","pending","d","2026-01-01T12:00:00Z","admin","2026-01-01T11:55:00Z","c","created"])
now = "2026-01-01T12:15:00Z"
cur = con.execute("""
  UPDATE device_pair_requests
  SET status = ?, device_id = ?, decided_ts = ?, decided_by = ?
  WHERE id = ? AND status = 'pending'
""", ("accepted", "d", now, "admin", "a"))
print("rowcount_no_expiry_guard=", cur.rowcount)
print("status_after=", con.execute("SELECT status,device_id,expires_at_ts FROM device_pair_requests WHERE id='a'").fetchone())
con.execute("INSERT INTO device_pair_requests VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
    ["b","accepted","d2","2026-01-01T11:00:00Z","admin","2026-01-01T11:05:00Z","c","created"])
cur2 = con.execute("""
  UPDATE device_pair_requests
  SET status = ?, device_id = ?, decided_ts = ?, decided_by = ?
  WHERE id = ?
    AND status = 'pending'
    AND (? != 'accepted' OR expires_at_ts > ?)
""", ("accepted", "d2", now, "admin", "b", "accepted", now))
print("rowcount_already_decided=", cur2.rowcount)
PY

Repository: jaylfc/taOS

Length of output: 50367


Enforce the pairing TTL atomically in set_decision.

create() sets _TTL_SECS for expires_at_ts, and _live_status() treats overdue pending rows as expired, but set_decision() only checks status = 'pending'. An approve after the 10-minute TTL can set status = 'accepted' and mint device_id; the poll route then releases the scoped token. Add the expiry predicate to the accept UPDATE and update the docstring comment so it describes the expired decision path.

🤖 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/device_pair_requests_store.py` around lines 161 - 177, Update
set_decision’s pending-row UPDATE to allow acceptance only when expires_at_ts
has not passed, preserving atomicity and returning None for expired requests.
Ensure expired decisions cannot mint device_id, and revise the method’s
docstring comment to describe this expired decision behavior.

Comment on lines +51 to +53
class CreatePairRequest(BaseModel):
platform: str
display_name: str = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Bound display_name and platform at the model.

display_name has no length limit. The route stores it, embeds it in the Decision question at line 149, and embeds it in the notification message at line 174. An unauthenticated caller can therefore write an arbitrarily large string into the database and into the admin inbox.

platform is validated against _VALID_PLATFORMS at line 107, so a Literal type moves that check into the model and produces a 422 with a precise error.

🛡️ Proposed change
-from pydantic import BaseModel
+from typing import Literal
+
+from pydantic import BaseModel, Field
 
@@
 class CreatePairRequest(BaseModel):
-    platform: str
-    display_name: str = ""
+    platform: Literal["ios", "watchos", "android"]
+    display_name: str = Field(default="", max_length=64)

If you adopt Literal, keep _VALID_PLATFORMS as the single source and derive both from it, or remove the now-redundant check at lines 107-111.

🤖 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/device_pair_requests.py` around lines 51 - 53, Update
CreatePairRequest to enforce a maximum length for display_name and constrain
platform using a Literal derived from the existing _VALID_PLATFORMS source. Then
remove the redundant platform validation in the route, or retain it only if the
model constraint remains the single authoritative definition.

Comment on lines +81 to +85
def _requester_ip(request: Request) -> str:
client = request.client
if client is None:
return ""
return client.host or ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for forwarded-header or proxy middleware handling.
set -euo pipefail

rg -nP -C 4 'X-Forwarded-For|x-forwarded-for|ProxyHeaders|forwarded_allow_ips|client\.host'

Repository: jaylfc/taOS

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files under routes/configs:\n'
git ls-files | rg '(^|/)(routes|.*proxy|middleware|config|settings|docker|compose|nginx|caddy|traefik|haproxy|gunicorn|uvicorn|starlette|fastapi)' || true

printf '\nTarget file excerpt:\n'
sed -n '1,230p' tinyagentos/routes/device_pair_requests.py

printf '\nSearch request/client/address extraction patterns:\n'
rg -n -C 3 'request\.client|client\.host|X-Forwarded-For|x-forwarded-for|ProxyHeaders|forwarded_allow_ips|RemoteAddr|trust|headers\.get|miniconfig|config' . || true

Repository: jaylfc/taOS

Length of output: 50368


Avoid storing/proxying the raw client IP.

_requester_ip() still returns request.client.host, so this value can show the proxy/LB address instead of the peer address behind a reverse proxy. Check the deployment topology; if a trusted proxy is in front, read the original client IP from forwarded headers only for configured trusted proxy ranges.

🤖 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/device_pair_requests.py` around lines 81 - 85, Update
_requester_ip to avoid returning the raw request.client.host: when the immediate
client belongs to a configured trusted proxy range, extract the original client
IP from the appropriate forwarded header; otherwise use the direct peer address.
Preserve the empty result when no client is present and ensure forwarded headers
are ignored for untrusted clients.

Comment on lines +88 to +95
def _admin_user_id(request: Request) -> str:
"""Resolve the instance user the pairing Decision is addressed to (the
primary admin), mirroring the OS-level decider in routes/decisions.py."""
users = request.app.state.auth.list_users()
admins = [u for u in users if u.get("is_admin")]
if not admins:
return ""
return admins[0].get("id") or ""

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 | 🟠 Major | ⚡ Quick win

create_pair_request returns 200 even when no admin Decision was raised. Two paths reach the same outcome: no admin exists, or the Decision store call fails. In both cases the route still returns a pair_request_id, so the device polls a request that no admin can approve and no error surfaces to either party.

  • tinyagentos/routes/device_pair_requests.py#L88-L95: log a warning when _admin_user_id finds no admin, and make the caller fail with 503 instead of returning an unapprovable request id.
  • tinyagentos/routes/device_pair_requests.py#L164-L165: after logging the Decision failure, raise HTTPException(status_code=503, detail="pairing unavailable") rather than continuing to the success response.
📍 Affects 1 file
  • tinyagentos/routes/device_pair_requests.py#L88-L95 (this comment)
  • tinyagentos/routes/device_pair_requests.py#L164-L165
🤖 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/device_pair_requests.py` around lines 88 - 95, The pairing
route currently returns an unapprovable request when no admin exists or Decision
creation fails. In tinyagentos/routes/device_pair_requests.py lines 88-95,
update _admin_user_id to log a warning when no admin is found and make
create_pair_request return HTTP 503 instead of a request ID; at lines 164-165,
raise HTTPException(status_code=503, detail="pairing unavailable") immediately
after logging the Decision failure.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

_admin_user_id can return an empty string and silently disable approval.

If list_users() returns no admin, this helper returns "". The guard at line 144 then skips the Decision and the notification. The route still returns 200 with a pair_request_id, so the device polls a request that no admin will ever see.

Log a warning when no admin resolves, or reject the request with 503 so the caller learns that pairing is unavailable.

admins[0] also depends on the ordering of list_users(), which this file does not control. If ordering is not stable, the Decision can be addressed to a different admin between deployments. Select the admin by an explicit rule, for example the lowest created_at or a configured primary-admin id.

🤖 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/device_pair_requests.py` around lines 88 - 95, Update
_admin_user_id to select the primary admin deterministically using an explicit
rule such as the configured primary-admin ID or earliest created_at, and handle
the no-admin case explicitly. Ensure the pairing route does not return a
successful pair_request_id while silently skipping approval: log a warning
and/or reject the request with HTTP 503 before creating the request when no
admin resolves.

Comment on lines +113 to +136
store = _get_pair_requests_store(request)

# F4: cap TOTAL pending (not per-IP) -- mirrors the agent auth-request cap.
pending_count = await store.count_pending()
if pending_count >= _PENDING_CAP:
raise HTTPException(
status_code=429,
detail=(
f"too many pending pair requests ({pending_count} pending; "
f"resolve existing requests first)"
),
)

verify_code = _generate_verify_code()
requester_ip = _requester_ip(request)
display = (body.display_name or "").strip() or body.platform

record = await store.create(
platform=body.platform,
display_name=display,
verify_code=verify_code,
requester_ip=requester_ip,
)
pair_request_id = record["id"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

The pending cap is a check-then-write race on an unauthenticated endpoint.

count_pending at line 116 and store.create at line 130 are separate statements with an await between them. Concurrent POSTs all observe a count below _PENDING_CAP and all insert. The endpoint requires no authentication, so an attacker controls the concurrency directly and can push the live pending count well above 5.

Each excess row raises a blocking Decision to the admin at lines 146-163, so the failure mode is admin-inbox flooding, which is exactly the condition F4 is meant to prevent.

Enforce the cap inside a single SQL statement in the store, for example an INSERT ... SELECT guarded by a count subquery, and return the created row or None. The route then maps None to 429. A per-process asyncio.Lock around the count-and-create pair is a smaller fix, but it does not hold across multiple workers.

🤖 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/device_pair_requests.py` around lines 113 - 136, Move the
pending-cap enforcement from the separate count/create flow in the route to an
atomic store operation: update the store method used by the pairing endpoint to
perform a guarded INSERT that only succeeds when pending rows are below
_PENDING_CAP, returning the created record or None. Update the route around
store.create to map a None result to the existing 429 response, while preserving
the current success response and request fields.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Router not in register_all_routers 📜 Skill insight ⌂ Architecture
Description
The new device_pair_requests router that defines the /api/devices/pair-requests endpoints is not
registered in tinyagentos/routes/__init__.py’s register_all_routers(), so FastAPI will never
mount or serve these paths. This violates the requirement that new route modules be registered
centrally (rather than wired inline elsewhere), making the pairing feature unavailable.
Code

tinyagentos/routes/device_pair_requests.py[R42-43]

+router = APIRouter()
+logger = logging.getLogger(__name__)
Relevance

●●● Strong

Unregistered router makes endpoints unreachable; likely fixed before merge.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185138 requires that new routers be registered via register_all_routers() in
tinyagentos/routes/__init__.py. While tinyagentos/routes/device_pair_requests.py defines `router
= APIRouter()` and contains the pair-request endpoints, the central registration in
register_all_routers() shows multiple routers being included (e.g., decisions/devices and others)
but never includes device_pair_requests, meaning the application will not expose these routes at
runtime.

tinyagentos/routes/device_pair_requests.py[42-43]
tinyagentos/routes/init.py[80-88]
tinyagentos/routes/device_pair_requests.py[42-44]
tinyagentos/routes/device_pair_requests.py[98-218]
tinyagentos/routes/init.py[83-88]
tinyagentos/routes/init.py[301-330]
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
Ensure the new pairing router (`tinyagentos.routes.device_pair_requests.router`) is included in `register_all_routers()` so its `/api/devices/pair-requests` endpoints are actually mounted and reachable, and so it follows the central registration requirement (including standard CSRF dependency wiring).

## Issue Context
- PR Compliance ID 2185138 requires new routers to be registered in `tinyagentos/routes/__init__.py:register_all_routers()`.
- `tinyagentos/routes/device_pair_requests.py` defines `router = APIRouter()` with the pair-request endpoints, but `register_all_routers()` does not include it, so FastAPI never serves these paths.
- Routers are mounted centrally in `tinyagentos/routes/__init__.py` and should receive the standard CSRF dependency wiring (`dependencies=_csrf`) unless explicitly justified otherwise.

## Fix Focus Areas
- tinyagentos/routes/__init__.py[1-120]
- tinyagentos/routes/device_pair_requests.py[42-105]
- tinyagentos/routes/device_pair_requests.py[98-218]

## Proposed fix
1. In `tinyagentos/routes/__init__.py`, import the router:
  `from tinyagentos.routes.device_pair_requests import router as device_pair_requests_router`.
2. Add it to `register_all_routers()` with the standard dependencies, e.g.:
  `app.include_router(device_pair_requests_router, dependencies=_csrf)` (or explicitly justify using a different dependency set).

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


2. Store not wired to app.state 📜 Skill insight ⌂ Architecture
Description
The pair-request routes depend on request.app.state.device_pair_requests, but the application
wiring in tinyagentos/app.py does not create or attach a DevicePairRequestsStore to that state
field. As a result, calling these endpoints will raise a runtime error and return 500s, violating
the requirement that new stores be attached via the lifespan/app wiring.
Code

tinyagentos/routes/device_pair_requests.py[R60-64]

+def _get_pair_requests_store(request: Request) -> DevicePairRequestsStore:
+    store = getattr(request.app.state, "device_pair_requests", None)
+    if store is None:
+        raise RuntimeError("device_pair_requests store not on app.state")
+    return store
Relevance

●●● Strong

Missing app.state wiring is a runtime-breaker; team usually fixes such integration gaps.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185458 requires that any new store be instantiated, initialized, and attached to
app.state in the lifespan/app wiring; however, the route helper _get_pair_requests_store()
explicitly checks for request.app.state.device_pair_requests and raises a
RuntimeError("device_pair_requests store not on app.state") when it is missing. In contrast, the
tinyagentos/app.py wiring shows multiple stores being assigned to app.state (e.g.,
app.state.device_store = device_store and other similar assignments) and initialized during
lifespan(), but there is no corresponding app.state.device_pair_requests assignment (nor a
matching init), demonstrating the mismatch that will cause runtime failures when the router is
mounted and exercised.

tinyagentos/routes/device_pair_requests.py[60-64]
tinyagentos/app.py[1619-1626]
tinyagentos/app.py[421-440]
tinyagentos/app.py[1589-1624]
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
`_get_pair_requests_store()` hard-requires `request.app.state.device_pair_requests`, but the app startup wiring in `tinyagentos/app.py` never constructs, initializes (`init()`), or assigns a `DevicePairRequestsStore` to that field. Update the app wiring so the store is created, attached to `app.state`, initialized during `lifespan()`, and shut down/closed if your store pattern requires it.

## Issue Context
- PR Compliance ID 2185458 requires new stores to be attached to `app.state` in the lifespan/app wiring.
- The route helper `_get_pair_requests_store()` expects `request.app.state.device_pair_requests` and raises at runtime if it is absent, which would surface as 500s for requests.
- `tinyagentos/app.py` already wires many SQLite-backed stores by instantiating them in `create_app()`, assigning them onto `app.state` (e.g., `app.state.device_store = device_store`), and calling `await ...init()` during `lifespan()`, but there is no equivalent wiring for `device_pair_requests`.

## Fix Focus Areas
- tinyagentos/app.py[401-606]
- tinyagentos/app.py[500-700]
- tinyagentos/app.py[1589-1645]
- tinyagentos/app.py[1608-1630]
- tinyagentos/routes/device_pair_requests.py[60-65]
- tinyagentos/device_pair_requests_store.py[96-110]

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


3. No device_pairing handler 🐞 Bug ≡ Correctness
Description
create_pair_request creates a Decision with metadata kind="device_pairing", but the decisions answer
path only runs side effects for execution_gate/delegation_gate/app_grant. As a result,
approving/denying the Decision will not transition the pair request or mint a device, so polling
cannot ever reach "accepted" with a token.
Code

tinyagentos/routes/device_pair_requests.py[R155-158]

+                metadata={
+                    "kind": "device_pairing",
+                    "pair_request_id": pair_request_id,
+                },
Relevance

●●● Strong

Missing Decision kind handler breaks intended flow; functional bug likely addressed.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The decision is created with kind=device_pairing, but decision answering only dispatches three other
kinds; there is no code path that touches DevicePairRequestsStore.set_decision() for device pairing.

tinyagentos/routes/device_pair_requests.py[142-163]
tinyagentos/routes/decisions.py[408-417]

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

### Issue description
Decisions answers are not wired to the new device-pairing workflow: there is no handler that interprets `metadata.kind == "device_pairing"` to mint a device and update `device_pair_requests`.

### Issue Context
`create_pair_request()` writes `metadata={"kind":"device_pairing","pair_request_id":...}` and the module docstring claims an `_apply_device_pairing_grant` exists, but `answer_decision()` only invokes three handlers.

### Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[138-163]
- tinyagentos/routes/decisions.py[393-417]
- tinyagentos/device_pair_requests_store.py[140-178]
- tinyagentos/device_store.py[43-66]

### Proposed fix
1. Add a new handler in `routes/decisions.py` (e.g. `_apply_device_pairing_grant`) dispatched from `answer_decision()`.
2. Validate metadata contains `pair_request_id` and that the decision is approve/deny.
3. On approve: mint a device via `DeviceStore.register(user_id=decision.user_id, platform=..., ...)`, then atomically set pair request to accepted with `set_decision(..., status="accepted", device_id=..., decided_by=answered_by)`.
4. On deny: `set_decision(..., status="denied", decided_by=answered_by)`.
5. Enforce TTL at approve time (reject or mark expired) before minting/accepting.

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


View more (1)
4. DevicePairRequestsStore imported in route 📜 Skill insight ⌂ Architecture
Description
tinyagentos/routes/device_pair_requests.py directly imports the store module/class instead of
accessing stores only via request.app.state, violating the route-store access rule. This increases
coupling/circular-import risk and bypasses the intended lifespan-managed store wiring pattern.
Code

tinyagentos/routes/device_pair_requests.py[R36-40]

+from tinyagentos.device_pair_requests_store import (
+    DevicePairRequestsStore,
+    _PENDING_CAP,
+    _live_status,
+)
Relevance

●● Moderate

No direct precedent on banning store imports in routes; pattern of using app.state exists.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185099 forbids direct imports of store modules/classes in route modules. The route
file imports DevicePairRequestsStore from tinyagentos.device_pair_requests_store rather than
treating the store as an app-wired dependency accessed only via request.app.state.

tinyagentos/routes/device_pair_requests.py[36-40]
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
Routes must not directly import store modules/classes; they must access stores via `request.app.state` only.

## Issue Context
`tinyagentos/routes/device_pair_requests.py` imports `DevicePairRequestsStore`, `_PENDING_CAP`, and `_live_status` from `tinyagentos/device_pair_requests_store.py`.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[36-65]
- tinyagentos/device_pair_requests_store.py[29-93]

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



Remediation recommended

5. Missing tests for new route 📜 Skill insight ⚙ Maintainability
Description
A new route module tinyagentos/routes/device_pair_requests.py was added without corresponding
tests that mirror the module structure and cover the new endpoints. This risks untested
auth/expiry/token-claim behavior in a security-sensitive flow.
Code

tinyagentos/routes/device_pair_requests.py[R98-100]

+@router.post("/api/devices/pair-requests")
+async def create_pair_request(request: Request, body: CreatePairRequest):
+    """Submit a pairing request from an external device/app.
Relevance

●●● Strong

Team frequently accepts adding tests; PR warning explicitly flags missing tests.

PR-#449

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185311 requires a test file tests/test_<module>.py for newly added route
modules. The PR adds a new route module, and the existing device routes test file only covers
/api/devices/register, listing, push-token update, and revoke flows.

tinyagentos/routes/device_pair_requests.py[1-8]
tests/routes/test_devices.py[1-49]
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
New route modules must have a corresponding test file that mirrors the module name.

## Issue Context
This PR adds `tinyagentos/routes/device_pair_requests.py`. Existing device route tests (`tests/routes/test_devices.py`) do not cover the new `/api/devices/pair-requests` endpoints.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[98-218]
- tests/routes/test_devices.py[1-49]
- tests/test_device_pair_requests.py[1-200]

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


6. Unauth device data leak 🐞 Bug ⛨ Security
Description
The unauthenticated poll endpoint returns the entire device row except scoped_token, which includes
user_id and push_token from DeviceStore.get(). Anyone who obtains the pair_request_id capability can
learn these unnecessary internal/sensitive fields.
Code

tinyagentos/routes/device_pair_requests.py[R211-213]

+                device_safe = {k: v for k, v in device.items() if k != "scoped_token"}
+                result["device"] = device_safe
+                # scoped_token is released exactly once (F3 / design "ONCE").
Relevance

●●● Strong

Security hardening for unauth endpoints is typically accepted; avoid leaking internal device fields.

PR-#301

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The poll route only strips scoped_token, while DeviceStore.get() explicitly includes user_id and
push_token among returned columns; those will be included in the poll response today.

tinyagentos/routes/device_pair_requests.py[203-217]
tinyagentos/device_store.py[10-19]

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

### Issue description
`get_pair_request()` returns `device_safe` built by removing only `scoped_token`, but the device row includes fields that should not be exposed on an unauthenticated capability endpoint (notably `user_id` and `push_token`).

### Issue Context
`DeviceStore.get()` returns `_FULL_COLS` including `user_id` and `push_token`.

### Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[203-217]
- tinyagentos/device_store.py[10-19]

### Proposed fix
Replace the current `device_safe = {k: v for k, v in device.items() if k != "scoped_token"}` with an explicit allowlist appropriate for pairing, e.g. `{ "device_id": ..., "platform": ..., "display_name": ..., "registered_at": ..., "last_seen": ..., "revoked": ... }`, and exclude `user_id` and `push_token` (and any other nonessential fields).

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


7. Docs not updated for routes 📜 Skill insight § Compliance
Description
A new route module was added under tinyagentos/routes/ but docs/agent-coordination.md does not
document the new /api/devices/pair-requests endpoints. This violates the doc-gate requirement for
route-module changes.
Code

tinyagentos/routes/device_pair_requests.py[R3-7]

+"""Routes for the device pairing consent loop (taOS S4e).
+
+POST /api/devices/pair-requests            -- unauthenticated; create a request + Decision
+GET  /api/devices/pair-requests/{id}       -- unauthenticated; poll status / retrieve token
+
Relevance

●● Moderate

Docs fixes often accepted, but no close agent-coordination route doc-gate precedent found.

PR-#482

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185375 requires updating docs/agent-coordination.md when route modules are
added/removed. The new route module introduces /api/devices/pair-requests, while the agent
coordination doc discusses device scoped tokens and POST /api/devices/register but does not
mention the new pairing-request endpoints.

tinyagentos/routes/device_pair_requests.py[3-7]
docs/agent-coordination.md[238-252]
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
When adding/removing route modules under `tinyagentos/routes/`, update `docs/agent-coordination.md` to reflect the new endpoints, or include the required `Docs-Reviewed:` commit trailer when no doc change is needed.

## Issue Context
This PR adds `/api/devices/pair-requests` endpoints but `docs/agent-coordination.md` contains device-token flow documentation that does not mention these endpoints.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[3-28]
- docs/agent-coordination.md[238-253]

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



Informational

8. Dead code in routes 🐞 Bug ⚙ Maintainability
Description
PairRequestIn plus constants _VERIFY_CODE_DIGITS and _MAX_DEVICES_PER_USER are defined but unused in
tinyagentos/routes/device_pair_requests.py. This adds misleading surface area and increases
maintenance risk as the constants drift from real behavior.
Code

tinyagentos/routes/device_pair_requests.py[R56-58]

+class PairRequestIn(BaseModel):
+    verify_code: str | None = None
+
Relevance

●●● Strong

Unused model/constants cleanup is low-risk and commonly accepted.

PR-#390

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The file defines a request model and constants that are not referenced by any route or helper in the
module.

tinyagentos/routes/device_pair_requests.py[45-58]
tinyagentos/routes/device_pair_requests.py[74-79]

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

### Issue description
Unused symbols in the new routes module: `PairRequestIn`, `_VERIFY_CODE_DIGITS`, `_MAX_DEVICES_PER_USER`.

### Issue Context
The verify-code generator hardcodes 6 digits and device caps are enforced elsewhere.

### Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[45-58]
- tinyagentos/routes/device_pair_requests.py[74-79]

### Proposed fix
Either delete the unused model/constants, or use them consistently (e.g., use `_VERIFY_CODE_DIGITS` in `_generate_verify_code()` and only keep `_MAX_DEVICES_PER_USER` if it is enforced in the pairing approval handler).

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


9. Store missing MIGRATIONS attribute 📜 Skill insight ⌂ Architecture
Description
DevicePairRequestsStore does not define a MIGRATIONS class attribute, contrary to the store
contract requiring explicit SCHEMA and MIGRATIONS. This can lead to inconsistent migration
behavior expectations across stores and violates the checklist rule.
Code

tinyagentos/device_pair_requests_store.py[R96-100]

+class DevicePairRequestsStore(BaseStore):
+    """Persistent store for device pairing (consent) requests."""
+
+    SCHEMA = SCHEMA
+
Relevance

● Weak

Close precedent rejected adding MIGRATIONS attribute on new BaseStore subclass.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185172 requires store classes to subclass BaseStore and define both SCHEMA and
MIGRATIONS. The new store class defines SCHEMA but has no MIGRATIONS attribute.

tinyagentos/device_pair_requests_store.py[96-100]
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
New store classes must explicitly declare both `SCHEMA` and `MIGRATIONS`.

## Issue Context
`DevicePairRequestsStore` sets `SCHEMA` but does not define `MIGRATIONS`.

## Fix Focus Areas
- tinyagentos/device_pair_requests_store.py[96-105]

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


10. Endpoints return untyped dicts 📜 Skill insight ✧ Quality
Description
The new routes return raw dicts without response_model=... response schemas, which bypasses
response validation and clear API contracts. This violates the requirement to use Pydantic models
for request/response payloads.
Code

tinyagentos/routes/device_pair_requests.py[R183-184]

+    # F3 / criterion 5: verify_code is returned ONLY here -- never on the poll.
+    return {"pair_request_id": pair_request_id, "verify_code": verify_code}
Relevance

● Weak

Close precedent rejected adding response_model/Pydantic responses; raw dicts tolerated.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185155 requires Pydantic models for request and response payloads. The POST
handler returns a raw dict ({"pair_request_id": ..., "verify_code": ...}) and the GET handler
returns a dynamically built dict, without any response_model contract.

tinyagentos/routes/device_pair_requests.py[183-184]
tinyagentos/routes/device_pair_requests.py[200-218]
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 handlers should declare Pydantic response models (and ideally return those models) instead of returning raw dicts.

## Issue Context
`create_pair_request` and `get_pair_request` both return dicts and do not specify `response_model=...`.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[98-105]
- tinyagentos/routes/device_pair_requests.py[183-218]

ⓘ 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 +36 to +40
from tinyagentos.device_pair_requests_store import (
DevicePairRequestsStore,
_PENDING_CAP,
_live_status,
)

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. devicepairrequestsstore imported in route 📜 Skill insight ⌂ Architecture

tinyagentos/routes/device_pair_requests.py directly imports the store module/class instead of
accessing stores only via request.app.state, violating the route-store access rule. This increases
coupling/circular-import risk and bypasses the intended lifespan-managed store wiring pattern.
Agent Prompt
## Issue description
Routes must not directly import store modules/classes; they must access stores via `request.app.state` only.

## Issue Context
`tinyagentos/routes/device_pair_requests.py` imports `DevicePairRequestsStore`, `_PENDING_CAP`, and `_live_status` from `tinyagentos/device_pair_requests_store.py`.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[36-65]
- tinyagentos/device_pair_requests_store.py[29-93]

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

Comment on lines +42 to +43
router = APIRouter()
logger = logging.getLogger(__name__)

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. Router not in register_all_routers 📜 Skill insight ⌂ Architecture

The new device_pair_requests router that defines the /api/devices/pair-requests endpoints is not
registered in tinyagentos/routes/__init__.py’s register_all_routers(), so FastAPI will never
mount or serve these paths. This violates the requirement that new route modules be registered
centrally (rather than wired inline elsewhere), making the pairing feature unavailable.
Agent Prompt
## Issue description
Ensure the new pairing router (`tinyagentos.routes.device_pair_requests.router`) is included in `register_all_routers()` so its `/api/devices/pair-requests` endpoints are actually mounted and reachable, and so it follows the central registration requirement (including standard CSRF dependency wiring).

## Issue Context
- PR Compliance ID 2185138 requires new routers to be registered in `tinyagentos/routes/__init__.py:register_all_routers()`.
- `tinyagentos/routes/device_pair_requests.py` defines `router = APIRouter()` with the pair-request endpoints, but `register_all_routers()` does not include it, so FastAPI never serves these paths.
- Routers are mounted centrally in `tinyagentos/routes/__init__.py` and should receive the standard CSRF dependency wiring (`dependencies=_csrf`) unless explicitly justified otherwise.

## Fix Focus Areas
- tinyagentos/routes/__init__.py[1-120]
- tinyagentos/routes/device_pair_requests.py[42-105]
- tinyagentos/routes/device_pair_requests.py[98-218]

## Proposed fix
1. In `tinyagentos/routes/__init__.py`, import the router:
   `from tinyagentos.routes.device_pair_requests import router as device_pair_requests_router`.
2. Add it to `register_all_routers()` with the standard dependencies, e.g.:
   `app.include_router(device_pair_requests_router, dependencies=_csrf)` (or explicitly justify using a different dependency set).

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

Comment on lines +60 to +64
def _get_pair_requests_store(request: Request) -> DevicePairRequestsStore:
store = getattr(request.app.state, "device_pair_requests", None)
if store is None:
raise RuntimeError("device_pair_requests store not on app.state")
return store

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. Store not wired to app.state 📜 Skill insight ⌂ Architecture

The pair-request routes depend on request.app.state.device_pair_requests, but the application
wiring in tinyagentos/app.py does not create or attach a DevicePairRequestsStore to that state
field. As a result, calling these endpoints will raise a runtime error and return 500s, violating
the requirement that new stores be attached via the lifespan/app wiring.
Agent Prompt
## Issue description
`_get_pair_requests_store()` hard-requires `request.app.state.device_pair_requests`, but the app startup wiring in `tinyagentos/app.py` never constructs, initializes (`init()`), or assigns a `DevicePairRequestsStore` to that field. Update the app wiring so the store is created, attached to `app.state`, initialized during `lifespan()`, and shut down/closed if your store pattern requires it.

## Issue Context
- PR Compliance ID 2185458 requires new stores to be attached to `app.state` in the lifespan/app wiring.
- The route helper `_get_pair_requests_store()` expects `request.app.state.device_pair_requests` and raises at runtime if it is absent, which would surface as 500s for requests.
- `tinyagentos/app.py` already wires many SQLite-backed stores by instantiating them in `create_app()`, assigning them onto `app.state` (e.g., `app.state.device_store = device_store`), and calling `await ...init()` during `lifespan()`, but there is no equivalent wiring for `device_pair_requests`.

## Fix Focus Areas
- tinyagentos/app.py[401-606]
- tinyagentos/app.py[500-700]
- tinyagentos/app.py[1589-1645]
- tinyagentos/app.py[1608-1630]
- tinyagentos/routes/device_pair_requests.py[60-65]
- tinyagentos/device_pair_requests_store.py[96-110]

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

Comment on lines +98 to +100
@router.post("/api/devices/pair-requests")
async def create_pair_request(request: Request, body: CreatePairRequest):
"""Submit a pairing request from an external device/app.

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. Missing tests for new route 📜 Skill insight ⚙ Maintainability

A new route module tinyagentos/routes/device_pair_requests.py was added without corresponding
tests that mirror the module structure and cover the new endpoints. This risks untested
auth/expiry/token-claim behavior in a security-sensitive flow.
Agent Prompt
## Issue description
New route modules must have a corresponding test file that mirrors the module name.

## Issue Context
This PR adds `tinyagentos/routes/device_pair_requests.py`. Existing device route tests (`tests/routes/test_devices.py`) do not cover the new `/api/devices/pair-requests` endpoints.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[98-218]
- tests/routes/test_devices.py[1-49]
- tests/test_device_pair_requests.py[1-200]

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

Comment on lines +3 to +7
"""Routes for the device pairing consent loop (taOS S4e).

POST /api/devices/pair-requests -- unauthenticated; create a request + Decision
GET /api/devices/pair-requests/{id} -- unauthenticated; poll status / retrieve 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.

Remediation recommended

7. Docs not updated for routes 📜 Skill insight § Compliance

A new route module was added under tinyagentos/routes/ but docs/agent-coordination.md does not
document the new /api/devices/pair-requests endpoints. This violates the doc-gate requirement for
route-module changes.
Agent Prompt
## Issue description
When adding/removing route modules under `tinyagentos/routes/`, update `docs/agent-coordination.md` to reflect the new endpoints, or include the required `Docs-Reviewed:` commit trailer when no doc change is needed.

## Issue Context
This PR adds `/api/devices/pair-requests` endpoints but `docs/agent-coordination.md` contains device-token flow documentation that does not mention these endpoints.

## Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[3-28]
- docs/agent-coordination.md[238-253]

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

Comment on lines +155 to +158
metadata={
"kind": "device_pairing",
"pair_request_id": pair_request_id,
},

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

8. No device_pairing handler 🐞 Bug ≡ Correctness

create_pair_request creates a Decision with metadata kind="device_pairing", but the decisions answer
path only runs side effects for execution_gate/delegation_gate/app_grant. As a result,
approving/denying the Decision will not transition the pair request or mint a device, so polling
cannot ever reach "accepted" with a token.
Agent Prompt
### Issue description
Decisions answers are not wired to the new device-pairing workflow: there is no handler that interprets `metadata.kind == "device_pairing"` to mint a device and update `device_pair_requests`.

### Issue Context
`create_pair_request()` writes `metadata={"kind":"device_pairing","pair_request_id":...}` and the module docstring claims an `_apply_device_pairing_grant` exists, but `answer_decision()` only invokes three handlers.

### Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[138-163]
- tinyagentos/routes/decisions.py[393-417]
- tinyagentos/device_pair_requests_store.py[140-178]
- tinyagentos/device_store.py[43-66]

### Proposed fix
1. Add a new handler in `routes/decisions.py` (e.g. `_apply_device_pairing_grant`) dispatched from `answer_decision()`.
2. Validate metadata contains `pair_request_id` and that the decision is approve/deny.
3. On approve: mint a device via `DeviceStore.register(user_id=decision.user_id, platform=..., ...)`, then atomically set pair request to accepted with `set_decision(..., status="accepted", device_id=..., decided_by=answered_by)`.
4. On deny: `set_decision(..., status="denied", decided_by=answered_by)`.
5. Enforce TTL at approve time (reject or mark expired) before minting/accepting.

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

Comment on lines +211 to +213
device_safe = {k: v for k, v in device.items() if k != "scoped_token"}
result["device"] = device_safe
# scoped_token is released exactly once (F3 / design "ONCE").

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

9. Unauth device data leak 🐞 Bug ⛨ Security

The unauthenticated poll endpoint returns the entire device row except scoped_token, which includes
user_id and push_token from DeviceStore.get(). Anyone who obtains the pair_request_id capability can
learn these unnecessary internal/sensitive fields.
Agent Prompt
### Issue description
`get_pair_request()` returns `device_safe` built by removing only `scoped_token`, but the device row includes fields that should not be exposed on an unauthenticated capability endpoint (notably `user_id` and `push_token`).

### Issue Context
`DeviceStore.get()` returns `_FULL_COLS` including `user_id` and `push_token`.

### Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[203-217]
- tinyagentos/device_store.py[10-19]

### Proposed fix
Replace the current `device_safe = {k: v for k, v in device.items() if k != "scoped_token"}` with an explicit allowlist appropriate for pairing, e.g. `{ "device_id": ..., "platform": ..., "display_name": ..., "registered_at": ..., "last_seen": ..., "revoked": ... }`, and exclude `user_id` and `push_token` (and any other nonessential fields).

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

Comment on lines +56 to +58
class PairRequestIn(BaseModel):
verify_code: str | 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.

Informational

10. Dead code in routes 🐞 Bug ⚙ Maintainability

PairRequestIn plus constants _VERIFY_CODE_DIGITS and _MAX_DEVICES_PER_USER are defined but unused in
tinyagentos/routes/device_pair_requests.py. This adds misleading surface area and increases
maintenance risk as the constants drift from real behavior.
Agent Prompt
### Issue description
Unused symbols in the new routes module: `PairRequestIn`, `_VERIFY_CODE_DIGITS`, `_MAX_DEVICES_PER_USER`.

### Issue Context
The verify-code generator hardcodes 6 digits and device caps are enforced elsewhere.

### Fix Focus Areas
- tinyagentos/routes/device_pair_requests.py[45-58]
- tinyagentos/routes/device_pair_requests.py[74-79]

### Proposed fix
Either delete the unused model/constants, or use them consistently (e.g., use `_VERIFY_CODE_DIGITS` in `_generate_verify_code()` and only keep `_MAX_DEVICES_PER_USER` if it is enforced in the pairing approval handler).

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

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-kilo review

VERDICT: Code is largely correct with solid atomic transitions and security-by-design (verify_code never server-checked, one-time token claim, expiry enforced at approve time). Minor issues below.

  • tinyagentos/routes/device_pair_requests.py:138_admin_user_id picks the first admin arbitrarily; if multiple admins exist, the "primary" is not guaranteed. Consider sorting by creation time or using a designated primary admin flag.

  • tinyagentos/routes/device_pair_requests.py:162request.client.host trusts direct connection; behind a proxy/load balancer this returns the proxy IP. Should read X-Forwarded-For or X-Real-IP headers (with trusted proxy config).

  • tinyagentos/routes/device_pair_requests.py:197-201 — Decision creation swallows all exceptions with a warning log. If Decision store is down, the pairing request is created but no admin is notified — silent failure. At minimum, re-raise or return 500 so the caller knows approval cannot proceed.

  • tinyagentos/routes/device_pair_requests.py:207-214 — Notification failure is silently ignored (pass). If notifications are critical for audit, this should at least log.

  • tinyagentos/device_pair_requests_store.py:114_is_expired returns False on ValueError parsing expires_at_ts. A malformed timestamp (should never happen) would treat the request as non-expired. Consider logging or raising.

  • tinyagentos/device_pair_requests_store.py:145count_pending uses expires_at_ts > now_iso (strict), while _is_expired uses exp <= now (inclusive). A request expiring exactly at now is counted as pending but reported as expired. Use consistent boundary (>= in count or > in expiry check).

  • tinyagentos/routes/device_pair_requests.py:42PairRequestIn model is defined but never used. Remove or use it.

  • tinyagentos/device_pair_requests_store.py — No cleanup job for expired/denied requests; they accumulate indefinitely. Add a periodic vacuum or TTL-based deletion.

  • Both files — No tests included in diff. Critical paths (atomic set_decision race, claim_scoped_token race, expiry-at-approve, verify_code never leaked, platform whitelist, pending cap) need integration tests.
    VERDICT: Code is largely correct with solid atomic transitions and security-by-design (verify_code never server-checked, one-time token claim, expiry enforced at approve time). Minor issues below.

  • tinyagentos/routes/device_pair_requests.py:138_admin_user_id picks the first admin arbitrarily; if multiple admins exist, the "primary" is not guaranteed. Consider sorting by creation time or using a designated primary admin flag.

  • tinyagentos/routes/device_pair_requests.py:162request.client.host trusts direct connection; behind a proxy/load balancer this returns the proxy IP. Should read X-Forwarded-For or X-Real-IP headers (with trusted proxy config).

  • tinyagentos/routes/device_pair_requests.py:197-201 — Decision creation swallows all exceptions with a warning log. If Decision store is down, the pairing request is created but no admin is notified — silent failure. At minimum, re-raise or return 500 so the caller knows approval cannot proceed.

  • tinyagentos/routes/device_pair_requests.py:207-214 — Notification failure is silently ignored (pass). If notifications are critical for audit, this should at least log.

  • tinyagentos/device_pair_requests_store.py:114_is_expired returns False on ValueError parsing expires_at_ts. A malformed timestamp (should never happen) would treat the request as non-expired. Consider logging or raising.

  • tinyagentos/device_pair_requests_store.py:145count_pending uses expires_at_ts > now_iso (strict), while _is_expired uses exp <= now (inclusive). A request expiring exactly at now is counted as pending but reported as expired. Use consistent boundary (>= in count or > in expiry check).

  • tinyagentos/routes/device_pair_requests.py:42PairRequestIn model is defined but never used. Remove or use it.

  • tinyagentos/device_pair_requests_store.py — No cleanup job for expired/denied requests; they accumulate indefinitely. Add a periodic vacuum or TTL-based deletion.

  • Both files — No tests included in diff. Critical paths (atomic set_decision race, claim_scoped_token race, expiry-at-approve, verify_code never leaked, platform whitelist, pending cap) need integration tests.

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

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-orB review

Now I'll provide the code review of the git diff.
Now I'll provide the code review based on my analysis of the diff.
VERDICT: PASS with minor issues

  • tinyagentos/device_pair_requests_store.py:179: _live_status returns "expired" for expired pending records but doesn't persist it; set_decision with status="expired" is never called automatically, so expired records remain "pending" in DB until manually cleaned up or decided — consider a background janitor or auto-transition on read
  • tinyagentos/device_pair_requests_store.py:197: create returns await self.get(pair_request_id) which strips verify_code via _SAFE_COLS, but docstring says "Returns the full record" — caller expects verify_code (routes uses it) but get() excludes it; this works because create() returns before get() is called in routes, but the store.get() contract is inconsistent
  • tinyagentos/routes/device_pair_requests.py:59: _admin_user_id picks first admin arbitrarily; if multiple admins exist, Decision goes to only one — should notify all admins or use a defined primary
  • tinyagentos/routes/device_pair_requests.py:113: Decision creation is best-effort (swallows exceptions) but the pair request is already created — if Decision fails silently, admin never sees it and request expires unnoticed; should either fail the request or have a retry/alert mechanism
  • tinyagentos/device_pair_requests_store.py:145: list_pending uses SELECT * then strips verify_code in Python; should use _SAFE_COLS for consistency and to avoid accidental leakage if strip logic changes
  • tinyagentos/routes/device_pair_requests.py:179: get_pair_request returns device dict excluding scoped_token but includes all other device fields — verify DeviceStore.get() doesn't return sensitive fields (e.g., raw secrets) that shouldn't be exposed to the pairing device
  • tinyagentos/device_pair_requests_store.py:83: _is_expired returns False on ValueError parsing expires_at_ts — malformed timestamps treated as never-expiring; should log warning and treat as expired for safety
  • tinyagentos/routes/device_pair_requests.py:24: _VALID_PLATFORMS is a frozenset but error message uses sorted(_VALID_PLATFORMS) — frozenset is unordered but sorted works; minor style: use tuple/list for ordered constants

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

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Mechanical note on the doc-gate red: the gate gained on_modify rules today (#2236), and this PR modifies files under tinyagentos/routes/, so it now needs either a CHANGELOG.md line / agent-doc update in the changeset or a 'Docs-Reviewed: ' commit trailer. The red is the new gate working as intended, not a flake. Kilo's red IS the known rate-limit noise. The 14 bot findings still need adjudication before this merges.

@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.

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