Skip to content

tsk-6lqhaa [OPEN] A2A read receipts: delivered + seen marks per (mes - #232

Closed
jaylfc wants to merge 1 commit into
masterfrom
exec/tsk-6lqhaa
Closed

tsk-6lqhaa [OPEN] A2A read receipts: delivered + seen marks per (mes#232
jaylfc wants to merge 1 commit into
masterfrom
exec/tsk-6lqhaa

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Autonomous build of board card tsk-6lqhaa.

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:
taosmd/api.py | 4 ++
taosmd/http_server.py | 110 ++++++++++++++++++++++++++++++++++-
taosmd/migrations.py | 18 ++++++
taosmd/receipts.py | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++
taosmd/service.py | 98 +++++++++++++++++++++++++++++++
5 files changed, 387 insertions(+), 1 deletion(-)


Summary by Gitar

  • New features:
    • Added ReceiptStore in taosmd/receipts.py for tracking A2A delivery and seen receipts with idempotent insertion and monotonic updates
    • Added HTTP endpoints in taosmd/http_server.py for recording seen receipts, querying message receipts, and administrative pruning
    • Added service-level wrappers and database migrations for the new a2a-receipts.db store

This will update automatically on new commits.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jaylfc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f2c2fbe-ad37-4f88-a526-7eced165eaad

📥 Commits

Reviewing files that changed from the base of the PR and between dac15f0 and d963f6f.

📒 Files selected for processing (5)
  • taosmd/api.py
  • taosmd/http_server.py
  • taosmd/migrations.py
  • taosmd/receipts.py
  • taosmd/service.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gitar-bot

gitar-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Important

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

Gitar

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Good progress and a real improvement on the rejected PR 224, which was entirely unwired dead code. This one compiles, is wired into service.py, http_server.py and api.py, and registers the migration with a proper baseline function rather than smuggling a table in via a schema constant. That was the main thing I asked for.

It cannot merge yet: the diff contains ZERO test files. Receipts are now on the critical path (priority 90) because the entry badges in the taOS collaboration design depend on an honest 'seen' state, and because without receipts an agent cannot know it has already handled a message and re-reads at full token cost. So this needs to be right rather than quick.

Tests required before I review the logic, so we only do that once:

  1. Delivered is written on the REAL delivery path, asserted through the path an agent actually uses, not by calling the store directly. That is the exact gap that made 224 worthless.
  2. Seen is distinct from delivered and does not imply it retroactively.
  3. Idempotency: marking the same (message, agent) twice does not duplicate or error.
  4. Per-agent keying: one agent's receipt for a message must not affect another agent's. PR 224 had a prune that deleted by message_id alone and wiped every agent's receipts for that message; prove this one does not.
  5. Migration over an EXISTING populated database, not a fresh one. The production bus runs on the Pi with real history.
  6. Cleanup or retention: if receipts accumulate per message per agent, say what bounds them. We are separately fixing a session store that grew to 37,077 rows because nothing pruned it.

RED-FIRST on 1 and 4.

One thing to state in the PR body rather than leave implicit: on a bus where the sender is self-claimed, a receipt is ADVISORY. Any client can mark any message as seen by any agent. That is acceptable for a badge, and unacceptable if anything ever gates on it. Say so in the docstring so the next reader does not assume more than it delivers. This is why the identity work (tsk-legqtr) sits above receipts in the ordering.

Also retitle: the title is the card id and truncated text. Sixth taosmd PR in a row with a title that does not describe the change; there is now a card for fixing that mechanically (tsk-nwyrtg).

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add A2A delivered/seen receipts with HTTP endpoints and SQLite backing store

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Persist per-(message, agent) delivered_at and seen_at receipts in a dedicated SQLite DB.
• Record delivered receipts for authenticated SSE subscribers; allow agents to mark messages seen.
• Expose read/delivery receipt query + admin pruning endpoints, wired through the service layer.
Diagram

graph TD
  C["A2A client/agent"] --> H["HTTP server (http_server.py)"] --> S["A2A service (service.py)"] --> R["ReceiptStore (receipts.py)"] --> D[("SQLite: a2a-receipts.db")]
  H --> J{{"Bearer JWT (sub)"}}
  M["Migration registry (migrations.py)"] --> D
  A["Admin client"] --> H

  subgraph Legend
    direction LR
    _svc["Module/handler"] ~~~ _db[("Database")] ~~~ _ext{{"External identity"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Store receipts in existing A2A/archive DB
  • ➕ Fewer SQLite files and migration registries to maintain
  • ➕ Simpler operational footprint (backup/restore, directory management)
  • ➖ Couples receipt write volume to message/archive storage concerns
  • ➖ Harder to tune/prune independently; increased contention risk on a shared DB
2. Record delivery via explicit ACK endpoint (not in SSE write path)
  • ➕ Avoids receipt writes on the request thread during streaming
  • ➕ More explicit delivery semantics (client confirms after processing)
  • ➖ Requires client changes and additional request traffic
  • ➖ Delivery fidelity depends on client correctness; SSE-only consumers may not ACK
3. Append-only receipt events + periodic compaction
  • ➕ Write-optimized; avoids UPDATEs/row locking patterns
  • ➕ Better auditability/history of transitions
  • ➖ More implementation complexity (compaction, querying latest state)
  • ➖ Overkill if current receipt volume is modest

Recommendation: Current approach (separate, minimal SQLite store with idempotent INSERT/guarded UPDATE) is a good fit for a lightweight server: it’s simple, queryable, and pruning is straightforward. The main tradeoff is the receipt write in the SSE handler thread; if this becomes a throughput issue, consider moving delivery marking behind an async queue or switching to an explicit ACK endpoint.

Files changed (5) +387 / -1

Enhancement (4) +369 / -1
api.pyInitialize and expose the receipts store via _ensure_stores() +4/-0

Initialize and expose the receipts store via _ensure_stores()

• Adds ReceiptStore creation, initialization, and inclusion in the shared stores dict so service functions can access it.

taosmd/api.py

http_server.pyAdd A2A receipt endpoints and mark delivered receipts during SSE streaming +109/-1

Add A2A receipt endpoints and mark delivered receipts during SSE streaming

• Documents and routes new receipt-related endpoints (PATCH seen, GET per-message receipts, GET per-agent receipt, and admin prune). Extracts authenticated agent identity from Bearer JWT 'sub' and records delivered receipts for identified SSE subscribers; adds handlers for receipt queries and seen marking.

taosmd/http_server.py

receipts.pyIntroduce ReceiptStore with delivered/seen semantics and pruning +158/-0

Introduce ReceiptStore with delivered/seen semantics and pruning

• Implements a new SQLite-backed store keyed by (message_id, agent_id) with monotonic delivered_at/seen_at behavior, message-level and per-agent queries, and TTL-style pruning by delivered_at.

taosmd/receipts.py

service.pyExpose service-layer receipt APIs with optional remote forwarding +98/-0

Expose service-layer receipt APIs with optional remote forwarding

• Adds async service wrappers for recording delivered/seen receipts, fetching receipts, and pruning old receipts; when a remote server is configured, calls are forwarded to the RemoteClient, otherwise executed against the local ReceiptStore.

taosmd/service.py

Other (1) +18 / -0
migrations.pyRegister a2a-receipts database migrations and file mapping +18/-0

Register a2a-receipts database migrations and file mapping

• Adds a baseline migration that executes the receipts schema, and registers the new logical DB name and filename (a2a-receipts.db) in the migration registry.

taosmd/migrations.py

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 13 rules

Grey Divider


Action required

1. Missing time import 🐞 Bug ≡ Correctness
Description
taosmd.service.a2a_record_seen/a2a_record_delivered call time.time() but taosmd/service.py
never imports time, causing NameError when ts is omitted (including the new PATCH
/a2a/receipts path). This makes seen receipt marking fail at runtime.
Code

taosmd/service.py[R640-644]

+    if ts is None:
+        ts = time.time()
+    remote = _get_remote(data_dir)
+    if remote is not None:
+        return await remote.a2a_record_delivered(message_id, agent_id, ts=ts)
Relevance

●●● Strong

Straight runtime NameError fixes are typically accepted; missing import is a clear correctness
issue.

PR-#180

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
service.py imports do not include time, but the new receipt functions call time.time() when
ts is None; the HTTP seen handler invokes a2a_record_seen without providing ts, triggering the
NameError.

taosmd/service.py[26-35]
taosmd/service.py[628-671]
taosmd/http_server.py[1656-1672]

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

## Issue description
`taosmd/service.py` uses `time.time()` in the new A2A receipt helpers, but the module does not import `time`. Any call that relies on the default `ts` (not passed explicitly) raises `NameError` and breaks the new seen-receipt endpoint.

## Issue Context
The HTTP handler `_handle_a2a_receipts_seen()` calls `service.a2a_record_seen(..., data_dir=...)` without a `ts`, so this is immediately user-facing.

## Fix Focus Areas
- taosmd/service.py[26-35]
- taosmd/service.py[628-671]
- taosmd/http_server.py[1656-1672]

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


2. Remote receipts methods missing 🐞 Bug ≡ Correctness
Description
When a remote server URL is configured, the new receipt service functions forward to remote.a2a_*
methods that do not exist on RemoteClient, causing AttributeError and disabling receipts in
remote mode. This impacts delivered/seen recording and receipt reads/pruning whenever
_get_remote() returns a client.
Code

taosmd/service.py[R665-668]

+    remote = _get_remote(data_dir)
+    if remote is not None:
+        return await remote.a2a_record_seen(message_id, agent_id, ts=ts)
+    stores = await _api._ensure_stores(data_dir)
Relevance

●●● Strong

Remote delegation needs matching RemoteClient methods; prior PRs show remote wrappers are expected
when service forwards.

PR-#139
PR-#212

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
service.py calls remote.a2a_record_seen(...) etc. when remote is configured, but RemoteClient
only defines A2A send/feed/channels/members methods and no receipt methods.

taosmd/service.py[628-723]
taosmd/remote.py[206-260]

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

## Issue description
`taosmd.service` now forwards receipt operations to `RemoteClient` when a remote server is configured, but `RemoteClient` does not implement `a2a_record_delivered`, `a2a_record_seen`, `a2a_get_receipts`, `a2a_get_receipt`, or `a2a_prune_receipts`. This will raise `AttributeError` at runtime in remote mode.

## Issue Context
Remote dispatch is a core feature of `taosmd.service` (transparent remote execution). The new receipt methods must either:
1) be implemented end-to-end (HTTP endpoints + RemoteClient methods), or
2) be explicitly blocked in remote mode with a controlled error.

## Fix Focus Areas
- taosmd/service.py[628-723]
- taosmd/remote.py[206-260]
- taosmd/http_server.py[1654-1700]

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


3. JWT sub not verified 🐞 Bug ⛨ Security
Description
_get_authenticated_agent_id() decodes the Bearer token with signature verification disabled and
trusts the unverified sub claim to attribute delivered/seen receipts, so a forged token can spoof
another agent’s identity in receipts. This bypasses the registry verifier’s signature/revocation
checks and undermines receipt integrity.
Code

taosmd/http_server.py[R746-749]

+                import jwt as _jwt  # noqa: PLC0415
+                unverified = _jwt.decode(token, options={"verify_signature": False})
+                sub = unverified.get("sub", "") or ""
+                return sub if sub else None
Relevance

●●● Strong

Team recently enforced verified Bearer-token auth/grants; unverified sub contradicts those security
patterns.

PR-#148
PR-#151

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The server currently decodes JWTs without verifying signatures to obtain sub, yet the registry
auth module provides RegistryVerifier.authorize() which verifies signature and revocation; the
unverified sub is then used to record delivered and seen receipts.

taosmd/http_server.py[729-752]
taosmd/http_server.py[1609-1644]
taosmd/http_server.py[1656-1672]
taosmd/registry_auth.py[48-80]
taosmd/registry_auth.py[139-145]

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

## Issue description
`taosmd/http_server.py::_get_authenticated_agent_id` extracts `sub` from a JWT decoded with `verify_signature=False` and returns it as the authenticated identity. This value is then used to write receipt rows, enabling identity spoofing.

## Issue Context
A registry verifier already exists (`RegistryVerifier.authorize`) and performs EdDSA signature verification + revocation checks. Receipt attribution should only use `sub` from verified claims.

## Fix Focus Areas
- taosmd/http_server.py[729-752]
- taosmd/http_server.py[1609-1644]
- taosmd/http_server.py[1656-1672]
- taosmd/registry_auth.py[48-80]
- taosmd/registry_auth.py[139-145]

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


View more (1)
4. Prune handler missing 🐞 Bug ≡ Correctness
Description
POST /a2a/admin/prune-receipts is routed to _handle_admin_a2a_prune_receipts() but that handler
is not defined on the HTTP handler class, causing an AttributeError (500) when the endpoint is
called. The route is therefore unusable as implemented.
Code

taosmd/http_server.py[R1089-1090]

+                elif method == "POST" and path == "/a2a/admin/prune-receipts":
+                    self._handle_admin_a2a_prune_receipts()
Relevance

●●● Strong

They routinely fix HTTP server dispatch bugs; missing handler causing 500 is likely corrected.

PR-#190
PR-#194

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The dispatcher calls _handle_admin_a2a_prune_receipts, but the admin A2A handler block ends
without defining it, so the method call cannot succeed.

taosmd/http_server.py[1083-1090]
taosmd/http_server.py[2119-2169]
taosmd/service.py[706-723]

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 router dispatches `POST /a2a/admin/prune-receipts` to `_handle_admin_a2a_prune_receipts()`, but the handler method is missing.

## Issue Context
Other admin A2A handlers exist in the same section (`_handle_admin_a2a_delete_channel`, `_handle_admin_a2a_rename_channel`, `_handle_admin_a2a_supersede_message`). Prune should follow the same pattern: enforce `_check_admin_token()`, parse `ttl_days`, compute cutoff timestamp, call `service.a2a_prune_receipts(...)`, return `{"pruned": int}`.

## Fix Focus Areas
- taosmd/http_server.py[1083-1090]
- taosmd/http_server.py[2119-2169]
- taosmd/service.py[706-723]

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



Remediation recommended

5. ReceiptStore skips migrations/WAL 🐞 Bug ☼ Reliability
Description
ReceiptStore.init() uses sqlite3.connect directly and does not run `migrations.migrate(...,
"a2a_receipts")`, unlike other stores, so WAL/busy_timeout pragmas and schema versioning are not
applied to the receipts DB. This increases the risk of database is locked under contention and
makes future upgrades/migrations for a2a-receipts.db unreliable.
Code

taosmd/receipts.py[R56-60]

+    async def init(self) -> None:
+        self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
+        self._conn.row_factory = sqlite3.Row
+        self._conn.executescript(SCHEMA)
+        self._conn.commit()
Relevance

●●● Strong

Repo standardized SQLite on _db.connect + migrations; bypassing WAL/busy_timeout/migrate is against
recent direction.

PR-#119
PR-#201
PR-#215

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Receipts currently uses a raw sqlite connection without the repo’s WAL/busy-timeout helper and
doesn’t run migrations, while the codebase’s standard pattern uses _db.connect +
migrations.migrate; the migrations registry explicitly adds a2a_receipts, reinforcing that this
DB is intended to be managed by migrations.

taosmd/receipts.py[56-61]
taosmd/_db.py[29-59]
taosmd/claims/store.py[40-47]
taosmd/migrations.py[305-318]
taosmd/migrations.py[374-421]
PR-#119

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

## Issue description
`ReceiptStore` initializes SQLite differently from the rest of the codebase: it bypasses `taosmd._db.connect` (WAL + busy timeout) and does not invoke `migrations.migrate` even though a migration registry entry for `a2a_receipts` was added.

## Issue Context
Other stores (e.g., `ClaimStore`) follow the standard pattern: `_db.connect(...)`, `executescript(SCHEMA)`, then `migrations.migrate(conn, "<logical_db_name>")`. The migrations registry now includes `a2a_receipts`, suggesting receipts should participate in the same versioning/upgrade system.

## Fix Focus Areas
- taosmd/receipts.py[56-61]
- taosmd/_db.py[29-59]
- taosmd/claims/store.py[40-47]
- taosmd/migrations.py[305-318]
- taosmd/migrations.py[374-421]

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


6. Receipts endpoints not archived 📘 Rule violation ☼ Reliability
Description
The new A2A receipt write/read endpoints update state and return results without any
archive.record(...) call, so these interactions can occur without being centrally archived. This
reduces auditability and violates the requirement to archive every interaction regardless of
success/failure paths.
Code

taosmd/service.py[R645-648]

+    stores = await _api._ensure_stores(data_dir)
+    receipt_store = stores["receipts"]
+    await receipt_store.record_delivered(message_id, agent_id, ts)
+    return {"ok": True}
Relevance

●● Moderate

No repo-history evidence found enforcing archive.record on every interaction; requirement seems
external/ambiguous.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1019881 requires every public interaction entry point to be archived via a
centralized archive.record(...) call. The newly added receipt handlers in taosmd/http_server.py
call receipt service methods and respond, and the new taosmd/service.py receipt methods write to
ReceiptStore but never call archive.record(...), so these interactions can occur without being
archived.

Rule 1019881: Archive every interaction with a single, centralized logger call
taosmd/http_server.py[1656-1672]
taosmd/http_server.py[1633-1644]
taosmd/service.py[628-648]

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 receipt-related HTTP entry points (and their service methods) do not call `archive.record(...)`, allowing interactions (delivered/seen marks and receipt reads) to happen without being archived.

## Issue Context
Compliance requires each public entry point that processes an interaction to invoke the centralized archiving utility exactly once per processed interaction, including error paths.

## Fix Focus Areas
- taosmd/http_server.py[1656-1700]
- taosmd/http_server.py[1633-1644]
- taosmd/service.py[628-723]

ⓘ 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 thread taosmd/service.py
Comment on lines +645 to +648
stores = await _api._ensure_stores(data_dir)
receipt_store = stores["receipts"]
await receipt_store.record_delivered(message_id, agent_id, ts)
return {"ok": True}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Receipts endpoints not archived 📘 Rule violation ☼ Reliability

The new A2A receipt write/read endpoints update state and return results without any
archive.record(...) call, so these interactions can occur without being centrally archived. This
reduces auditability and violates the requirement to archive every interaction regardless of
success/failure paths.
Agent Prompt
## Issue description
New receipt-related HTTP entry points (and their service methods) do not call `archive.record(...)`, allowing interactions (delivered/seen marks and receipt reads) to happen without being archived.

## Issue Context
Compliance requires each public entry point that processes an interaction to invoke the centralized archiving utility exactly once per processed interaction, including error paths.

## Fix Focus Areas
- taosmd/http_server.py[1656-1700]
- taosmd/http_server.py[1633-1644]
- taosmd/service.py[628-723]

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

Comment thread taosmd/service.py
Comment on lines +640 to +644
if ts is None:
ts = time.time()
remote = _get_remote(data_dir)
if remote is not None:
return await remote.a2a_record_delivered(message_id, agent_id, ts=ts)

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. Missing time import 🐞 Bug ≡ Correctness

taosmd.service.a2a_record_seen/a2a_record_delivered call time.time() but taosmd/service.py
never imports time, causing NameError when ts is omitted (including the new PATCH
/a2a/receipts path). This makes seen receipt marking fail at runtime.
Agent Prompt
## Issue description
`taosmd/service.py` uses `time.time()` in the new A2A receipt helpers, but the module does not import `time`. Any call that relies on the default `ts` (not passed explicitly) raises `NameError` and breaks the new seen-receipt endpoint.

## Issue Context
The HTTP handler `_handle_a2a_receipts_seen()` calls `service.a2a_record_seen(..., data_dir=...)` without a `ts`, so this is immediately user-facing.

## Fix Focus Areas
- taosmd/service.py[26-35]
- taosmd/service.py[628-671]
- taosmd/http_server.py[1656-1672]

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

Comment thread taosmd/service.py
Comment on lines +665 to +668
remote = _get_remote(data_dir)
if remote is not None:
return await remote.a2a_record_seen(message_id, agent_id, ts=ts)
stores = await _api._ensure_stores(data_dir)

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. Remote receipts methods missing 🐞 Bug ≡ Correctness

When a remote server URL is configured, the new receipt service functions forward to remote.a2a_*
methods that do not exist on RemoteClient, causing AttributeError and disabling receipts in
remote mode. This impacts delivered/seen recording and receipt reads/pruning whenever
_get_remote() returns a client.
Agent Prompt
## Issue description
`taosmd.service` now forwards receipt operations to `RemoteClient` when a remote server is configured, but `RemoteClient` does not implement `a2a_record_delivered`, `a2a_record_seen`, `a2a_get_receipts`, `a2a_get_receipt`, or `a2a_prune_receipts`. This will raise `AttributeError` at runtime in remote mode.

## Issue Context
Remote dispatch is a core feature of `taosmd.service` (transparent remote execution). The new receipt methods must either:
1) be implemented end-to-end (HTTP endpoints + RemoteClient methods), or
2) be explicitly blocked in remote mode with a controlled error.

## Fix Focus Areas
- taosmd/service.py[628-723]
- taosmd/remote.py[206-260]
- taosmd/http_server.py[1654-1700]

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

Comment thread taosmd/http_server.py
Comment on lines +746 to +749
import jwt as _jwt # noqa: PLC0415
unverified = _jwt.decode(token, options={"verify_signature": False})
sub = unverified.get("sub", "") or ""
return sub if sub else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Jwt sub not verified 🐞 Bug ⛨ Security

_get_authenticated_agent_id() decodes the Bearer token with signature verification disabled and
trusts the unverified sub claim to attribute delivered/seen receipts, so a forged token can spoof
another agent’s identity in receipts. This bypasses the registry verifier’s signature/revocation
checks and undermines receipt integrity.
Agent Prompt
## Issue description
`taosmd/http_server.py::_get_authenticated_agent_id` extracts `sub` from a JWT decoded with `verify_signature=False` and returns it as the authenticated identity. This value is then used to write receipt rows, enabling identity spoofing.

## Issue Context
A registry verifier already exists (`RegistryVerifier.authorize`) and performs EdDSA signature verification + revocation checks. Receipt attribution should only use `sub` from verified claims.

## Fix Focus Areas
- taosmd/http_server.py[729-752]
- taosmd/http_server.py[1609-1644]
- taosmd/http_server.py[1656-1672]
- taosmd/registry_auth.py[48-80]
- taosmd/registry_auth.py[139-145]

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

Comment thread taosmd/http_server.py
Comment on lines +1089 to +1090
elif method == "POST" and path == "/a2a/admin/prune-receipts":
self._handle_admin_a2a_prune_receipts()

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. Prune handler missing 🐞 Bug ≡ Correctness

POST /a2a/admin/prune-receipts is routed to _handle_admin_a2a_prune_receipts() but that handler
is not defined on the HTTP handler class, causing an AttributeError (500) when the endpoint is
called. The route is therefore unusable as implemented.
Agent Prompt
## Issue description
The router dispatches `POST /a2a/admin/prune-receipts` to `_handle_admin_a2a_prune_receipts()`, but the handler method is missing.

## Issue Context
Other admin A2A handlers exist in the same section (`_handle_admin_a2a_delete_channel`, `_handle_admin_a2a_rename_channel`, `_handle_admin_a2a_supersede_message`). Prune should follow the same pattern: enforce `_check_admin_token()`, parse `ttl_days`, compute cutoff timestamp, call `service.a2a_prune_receipts(...)`, return `{"pruned": int}`.

## Fix Focus Areas
- taosmd/http_server.py[1083-1090]
- taosmd/http_server.py[2119-2169]
- taosmd/service.py[706-723]

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

Comment thread taosmd/receipts.py
Comment on lines +56 to +60
async def init(self) -> None:
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
self._conn.row_factory = sqlite3.Row
self._conn.executescript(SCHEMA)
self._conn.commit()

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. Receiptstore skips migrations/wal 🐞 Bug ☼ Reliability

ReceiptStore.init() uses sqlite3.connect directly and does not run `migrations.migrate(...,
"a2a_receipts")`, unlike other stores, so WAL/busy_timeout pragmas and schema versioning are not
applied to the receipts DB. This increases the risk of database is locked under contention and
makes future upgrades/migrations for a2a-receipts.db unreliable.
Agent Prompt
## Issue description
`ReceiptStore` initializes SQLite differently from the rest of the codebase: it bypasses `taosmd._db.connect` (WAL + busy timeout) and does not invoke `migrations.migrate` even though a migration registry entry for `a2a_receipts` was added.

## Issue Context
Other stores (e.g., `ClaimStore`) follow the standard pattern: `_db.connect(...)`, `executescript(SCHEMA)`, then `migrations.migrate(conn, "<logical_db_name>")`. The migrations registry now includes `a2a_receipts`, suggesting receipts should participate in the same versioning/upgrade system.

## Fix Focus Areas
- taosmd/receipts.py[56-61]
- taosmd/_db.py[29-59]
- taosmd/claims/store.py[40-47]
- taosmd/migrations.py[305-318]
- taosmd/migrations.py[374-421]

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

@jaylfc

jaylfc commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

CHANGES REQUESTED — a dispatched handler does not exist, and there are no tests

Substantive review. Two blockers, one of which is the exact defect class this PR was reworked to fix.

Blocker 1 — POST /a2a/admin/prune-receipts dispatches to a handler that was never written

Checked by extracting every dispatched name from the source, rather than grepping for names I guessed:

handlers dispatched: 46   defined: 45
DISPATCHED BUT NOT DEFINED: ['_handle_admin_a2a_prune_receipts']

http_server.py:1090 calls self._handle_admin_a2a_prune_receipts(). No such method exists. The route is also advertised in the endpoint docstring table (line 153) and registered in the admin route list (line 781), so it is reachable — calling it raises AttributeError and returns a 500.

This is the same class as the previous rejection ("dispatch to handler methods that were never written"). It has recurred in the rework.

Blocker 2 — no tests, in a +387/-1 change

There are no test files in this PR at all.

The rework order was explicit: tests go in tests/ with asyncio markers, and every gate needs a deny-path test. The test check passing here proves nothing about this PR — it runs the existing suite, which never touches receipts. A missing handler on a reachable admin route is precisely what one test would have caught.

Also worth knowing: only one bot has actually reviewed this

CodeRabbit reports pass with the description "Review rate limited" — a pass status with no review behind it. Qodo is the only reviewer that produced findings. Do not read the checks page as two bot approvals.

What is correct and should survive the rework

Two of the three rework requirements are properly met:

Fix the handler, add the tests, and this is close.

@jaylfc

jaylfc commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Requesting changes. The store design is right (idempotent delivered mark, monotonic seen, prune by delivered_at) and the schema and service wrappers are clean. But four things are broken, and I found them by running the branch rather than reading it, so each one below has its evidence attached along with the positive control that makes the evidence mean something.

1. BLOCKER: the receipt identity is not verified, so any client can write receipts as any agent

_get_authenticated_agent_id decodes the Bearer token with verify_signature: False and returns the sub claim as the agent identity. Nothing ever checks the signature. _registry_verifier is tested for None and then never used.

This is not a theoretical gap. The correct API is already in the same class, 100 lines above: _resolve_project_from_token does the identical unverified peek, but only to extract the claimed identity, and then calls _registry_verifier.authorize(token, claimed_identity), with a comment saying "The real signature check happens inside authorize(); a bad token will still fail there." The receipts path does the peek and stops.

Proof, from the PR head. A token signed with a key the server has never seen:

forged token claims sub=taosmd-dev, signed with an UNKNOWN key

PATH UNDER TEST  _get_authenticated_agent_id(forged) = 'taosmd-dev'
  negative control, non-JWT garbage           = None
POSITIVE CONTROL authorize_sender(forged) = REJECTED: AuthError token verification failed: Signature verification failed
POSITIVE CONTROL authorize_sender(legit)  = ACCEPTED  <-- control can produce a presence

The last line is there on purpose: the control accepts a legitimately signed token, so its rejection of the forgery is a real rejection and not a broken harness. The negative control shows the function is not simply returning everything it is handed.

Consequence: PATCH /a2a/receipts lets anyone mark any message seen as any agent, and the SSE delivered mark is attributable to whoever asks. A receipt that can be forged is worse than no receipt, because the whole value of the feature is that the mark is evidence. Someone can claim I read a message I never received, or claim to have read one they ignored.

Fix: call _registry_verifier.authorize(token, raw_sub) and use the verified claims' sub. Return None on AuthError.

2. BLOCKER: POST /a2a/admin/prune-receipts 500s on every call, the handler was never written

The route is dispatched at http_server.py:1090 and added to _is_admin_route, and it is documented in the endpoint table as shipped. _handle_admin_a2a_prune_receipts does not exist.

POSITIVE CONTROL POST /a2a/admin/supersede-message -> (403, {"error": "admin surface requires a configured admin or server token"})
PATH UNDER TEST  POST /a2a/admin/prune-receipts    -> (500, {"error": "AttributeError: 'TaosmdHandler' object has no attribute '_handle_admin_a2a_prune_receipts'"})

The control is a sibling admin route on the same dispatcher that does have a handler. It answers 403, so the harness and the admin gate both work; the 500 is the missing handler alone. Note also that the crash happens before any admin check runs (each handler self-gates), and the response body leaks the internal attribute name.

service.a2a_prune_receipts exists and is fine. Only the HTTP handler is missing.

3. BLOCKER: the remote path calls five RemoteClient methods that do not exist

Every one of the new service wrappers forwards to remote.a2a_record_delivered, a2a_record_seen, a2a_get_receipts, a2a_get_receipt, a2a_prune_receipts. taosmd/remote.py is not touched by this PR and defines none of them:

$ git grep -n "def a2a_record_delivered\|def a2a_record_seen\|def a2a_get_receipts\|def a2a_get_receipt\|def a2a_prune_receipts\|def a2a_members" -- taosmd/remote.py
taosmd/remote.py:256:    async def a2a_members(self, *, channel: str, **_opts) -> list[str]:

a2a_members is in that pattern as the positive control: it is an existing forwarded call, it is found, so the query can produce a match. The five receipt methods are genuinely absent. Any install with a remote server URL configured, which is the whole hive deployment, raises AttributeError on every receipt call including the delivered mark inside the SSE loop.

4. No tests, which the card required and the PR body already flags

The automated warning in the description is correct: the diff changes no test file. Every defect above is one a test would have caught, including defect 1, which needs exactly one case asserting that a token signed by an unknown key does not yield an identity. Worth adding a red-first check for the forgery case specifically, so the fix is proved to close it rather than assumed to.

5. Not a blocker, but fix it while you are in here: receipts bypasses _db.connect

ReceiptStore.init calls sqlite3.connect(path, check_same_thread=False) directly. Every other persistent store in the package goes through taosmd/_db.py::connect, which exists precisely to enable WAL and set a busy timeout so concurrent writers wait rather than returning SQLITE_BUSY. receipts.py is the only file in taosmd/ that opens a raw connection this way.

That matters more here than anywhere else, because this is the most concurrently written store in the codebase: the SSE loop does an INSERT plus a commit per frame per subscriber, from a per-request thread, on one shared connection with check_same_thread=False and no lock. Rollback-journal mode plus multiple writer threads plus no busy timeout is the exact combination _db.py was written to prevent.

Two changes: use _db.connect, and either serialise writes on a lock or open per-thread connections. Also worth considering batching the delivered marks rather than committing per frame.

What is good and should not change

The store semantics are the part that is easy to get wrong and this got them right. INSERT OR IGNORE for delivered so a redelivering watcher cannot move the timestamp earlier, WHERE seen_at IS NULL so seen only ever moves from null to a value, prune keyed on delivered_at, and the documented decision that a raw-bus subscriber with no identity produces no mark rather than a mark attributed to nobody. Keep all of it. The migration registration and the service wrapper shape are also right.

@jaylfc

jaylfc commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Revision card filed: tsk-qo6tpb.

This PR is not being abandoned and it stays open. The card's STEP 0 squash-merges this branch so the work carries forward.

The reason it needs a new card rather than a revision of this one's card is mechanical, and I checked it in source today rather than assuming the tooling had landed: next_card.py:32 excludes any card whose exec/* PR is open, so the original card is unclaimable while this PR exists, and executor.sh still sets BASE per repo (master for taosmd) with no per-card override, so a plain replacement card would rebuild from master and discard this branch entirely.

STEP 0 uses git merge --squash, not git merge, because executor.sh:584 bounces any card whose branch contains a merge commit. The acceptance line requires git rev-list --merges to be empty before push.

Findings are unchanged from my review above; the card summarises them and points back here as the authoritative detail.

@jaylfc

jaylfc commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded by #270.

The review of this PR asked for the revision; tsk-qo6tpb produced it as #270 (identical five-file surface).

Verified before closing: #270 branches off master with its own commits and its changed-file set is a superset of this PR's, so closing this loses no content. The two would also conflict if both landed.

This PR was holding one of the 8 CI throttle slots on jaylfc/taosmd, which sat at 32/8 (locked). Reopen if the supersession is wrong.

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