Skip to content

feat(a2a): authenticated SSE stream proxy + since cursor (#1780 S3)#1854

Merged
jaylfc merged 1 commit into
devfrom
feat/invite-s3-a2a-stream
Jul 16, 2026
Merged

feat(a2a): authenticated SSE stream proxy + since cursor (#1780 S3)#1854
jaylfc merged 1 commit into
devfrom
feat/invite-s3-a2a-stream

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Slice S3 of the external-agent invite feature: the realtime A2A delivery leg.

  • Add GET /api/a2a/bus/stream?channel={c}&since={cursor}, an authenticated (agent-JWT or session) SSE proxy to the raw bus GET {bus}/a2a/stream?thread={c}&since={cursor}. It holds one upstream SSE connection per client and relays events verbatim, injecting a : ping heartbeat comment every 25s so idle streams survive intermediaries. Auth is gated exactly like the existing /api/a2a/bus/messages proxy (admin session / local token, or an active agent JWT holding a2a_receive, fail closed).
  • Add /api/a2a/bus/stream to _A2A_BUS_READ_PATHS in auth_middleware.py, mirroring the messages/channels allowlist entry (passthrough contract, no skeleton key).
  • Add a since query passthrough to the existing /api/a2a/bus/messages proxy (it previously dropped it), forwarded verbatim to the bus cursor.

The raw :7900 bus is never exposed directly; all bus access goes through the authenticated proxy, which forces the reader through the same gate as every other A2A read path.

Tests

tests/test_routes_a2a_bus_stream.py:

  • an agent-JWT bound to a project opens the stream and receives forwarded SSE frames (raw bus mocked)
  • the messages proxy forwards since
  • the stream proxy forwards since to the bus
  • unauthenticated request -> 401
  • missing channel -> 400
  • JWT without a2a_receive -> 403
  • raw /a2a/stream is not a registered route (404), proving the proxy is the only entry point

Run: /Volumes/NVMe/Users/jay/Development/tinyagentos/.venv/bin/python -m pytest tests/test_routes_a2a_bus_stream.py -q (7 passed).

Note: the pre-existing tests/test_a2a_bus.py and several e2e modules import optional deps (respx, playwright) not installed in this venv; that collection gap is unrelated to this change.

Add GET /api/a2a/bus/stream, an authenticated SSE proxy to the raw bus stream, gated exactly like the messages proxy. Add the stream path to the middleware read allowlist and forward the since cursor on the messages proxy.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 16, 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: 50 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 426f962f-2423-431e-9f94-72f4a31b6c86

📥 Commits

Reviewing files that changed from the base of the PR and between f688239 and 63c11ee.

📒 Files selected for processing (3)
  • tests/test_routes_a2a_bus_stream.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/routes/a2a_bus.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/invite-s3-a2a-stream

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 Jul 16, 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

break
# Drain any pending heartbeat that fired while we were
# forwarding real data: emit it before the next event.
if heartbeat.done():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Idle heartbeat is never emitted — defeats the stated purpose of the feature.

The heartbeat.done() check lives inside the async for line in upstream.aiter_lines() loop (line 180). If no upstream events ever arrive (the exact idle case this heartbeat exists for), the loop body never executes, so the : ping is never yielded and the connection is reaped by intermediaries exactly as the PR set out to prevent. Even between events, the ping is only emitted when an event line happens to arrive after 25s of silence — an idle stream of any length emits nothing. The heartbeat must fire on its own timer independently of inbound data (e.g. asyncio.wait/wait_for racing the line iterator against a sleep, or a dedicated timer that yields the ping), not be drained only when a line is received.

Suggested change
if heartbeat.done():
if await request.is_disconnected():
break
# NOTE: heartbeat must fire on its own timer outside this
# loop so idle streams still emit `: ping`.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

)
if line == "":
continue
yield f"{line}\n\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: aiter_lines() splits every newline into a separate iteration, so a multi-line SSE event (e.g. a data: field split across lines, or data: + blank-line continuations) would be re-framed as two separate one-line events, corrupting the relayed event. The upstream bus appears to emit one-line-per-event frames, so this is likely safe today, but it couples the proxy to that internal framing assumption. Consider relaying events by buffering until a blank line terminates an SSE event block rather than per-line, or assert/parse the upstream framing explicitly.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/routes/a2a_bus.py 185 Idle heartbeat is never emitted because the heartbeat.done() check lives inside the async for loop; quiet streams send no : ping and are reaped, defeating the feature's stated purpose.

SUGGESTION

File Line Issue
tinyagentos/routes/a2a_bus.py 193 Relaying line-by-line via aiter_lines() can corrupt multi-line SSE events; frame by event block instead of per line.
Files Reviewed (3 files)
  • tinyagentos/routes/a2a_bus.py - 2 issues
  • tinyagentos/auth_middleware.py - 0 issues
  • tests/test_routes_a2a_bus_stream.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 74.1K · Output: 4.7K · Cached: 101.1K

@jaylfc
jaylfc enabled auto-merge (squash) July 16, 2026 18:20
@jaylfc
jaylfc merged commit 6d9d9f5 into dev Jul 16, 2026
11 checks passed
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