Repository navigation
Conversation
Route session-bound A2A messages through existing ACP prompt and steering admission instead of transient model runs. BREAKING CHANGE: stdio v2 session-bound A2A returns an admission acknowledgment rather than execution output and rejects task tracking modes. Standalone and ACP v1 A2A execution are unchanged.
| if request.method() != Method::POST { | ||
| return self.inner.dispatch(request).await.map(Body::new); | ||
| } | ||
| let bytes = match to_bytes(Body::new(request.into_body()), 4 * 1024 * 1024).await { |
There was a problem hiding this comment.
[P1] Preserve the HTTP media-type boundary before admitting messages
With session admission enabled, every POST reaches this body parser without the Content-Type or A2A-Version checks performed by JsonRpcDispatcher. On a listener without a configured bearer token, a hostile web page can therefore send a valid admission request as a CORS-safelisted text/plain POST, without the custom version header, and inject a prompt into the bound ACP session in browsers that permit the loopback connection. Blocking response access does not prevent admission, and the shared listener does not check Origin. The pinned dispatcher previously rejected this request before executing it. Preserve the JSON media-type and version validation before admission, and add a regression test ensuring a text/plain request cannot enqueue a prompt or fall back to standalone execution.
For agents:
Validate the following issue, address if needed:
<comment>Session-admission POSTs bypass HTTP media-type and version validation, allowing browser-simple text/plain requests to enqueue executable prompts on an unauthenticated listener.</comment>
<file_context>
+ let Some(sessions) = &self.sessions else {
+ return self.inner.dispatch(request).await.map(Body::new);
+ };
+ if request.method() != Method::POST {
+ return self.inner.dispatch(request).await.map(Body::new);
+ }
+ let bytes = match to_bytes(Body::new(request.into_body()), 4 * 1024 * 1024).await {
+ Ok(bytes) => bytes,
+ Err(_) => {
+ let mut response =
+ Response::new(Body::from("A2A request body exceeds admission limit"));
+ *response.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
+ return response;
+ }
+ };
</file_context>
| if request.method() != Method::POST { | ||
| return self.inner.dispatch(request).await.map(Body::new); | ||
| } | ||
| let bytes = match to_bytes(Body::new(request.into_body()), 4 * 1024 * 1024).await { |
There was a problem hiding this comment.
[P2] Retain the request-body deadline in session-admission mode
The direct to_bytes(...).await retains the 4 MiB size limit but drops the pinned JsonRpcDispatcher's default 30-second body-read timeout. A client can send POST headers and an unfinished body below the cap, then leave this future pending indefinitely. The shared listener spawns a connection task for each accepted socket and supplies no replacement body deadline, so repeated incomplete requests retain tasks, sockets, and any accumulated body buffers until the clients disconnect or the server shuts down. Bound body collection with a deadline equivalent to the existing dispatcher, return a distinct timeout error without admission or standalone fallback, and cover an incomplete request body in a regression test.
For agents:
Validate the following issue, address if needed:
<comment>The session-admission body reader removes the dispatcher's 30-second deadline, allowing incomplete POST bodies to retain connection resources indefinitely.</comment>
<file_context>
+ if request.method() != Method::POST {
+ return self.inner.dispatch(request).await.map(Body::new);
+ }
+ let bytes = match to_bytes(Body::new(request.into_body()), 4 * 1024 * 1024).await {
+ Ok(bytes) => bytes,
+ Err(_) => {
+ let mut response =
+ Response::new(Body::from("A2A request body exceeds admission limit"));
+ *response.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
+ return response;
+ }
+ };
</file_context>
There was a problem hiding this comment.
Two issues remain: [P1] session-admission POSTs bypass HTTP media-type and A2A-Version validation, allowing browser-simple text/plain requests to enqueue prompts on an unauthenticated listener; [P2] the session-admission body reader drops the dispatcher's 30-second deadline, allowing incomplete POST bodies to retain connection resources indefinitely.
Summary
Route A2A messages on a stdio v2 session's listener through that session's existing ACP prompt and steering admission paths instead of creating a transient execution context. Return an explicit A2A Message acknowledging admission, with execution and output remaining in the recipient's ACP session.
Motivation
The A2A address shown in a session header previously executed incoming messages separately, without adding them to the displayed session's transcript or using its active turn. Incoming messages should enter the same session as TUI submissions.
Impact
This is a breaking change for session-bound stdio v2 listeners: responses acknowledge admission rather than returning execution output. Task continuation, streaming, push configuration, and
returnImmediately: trueare rejected; eventual A2A answer delivery is not included. Standalone--no-stdioand ACP v1 A2A execution retain their existing behavior.Technical details
Shared session ownership
Bind the listener explicitly to the stdio v2 session. Idle messages enter prompt admission; busy messages enter the existing steering machinery. A closed binding rejects admission rather than retargeting another session or falling back to transient execution.
Cancellation-safe injection
Use the ACP staged reservation and tracked-response activation boundary before acknowledging steering admission. Drain canceled pre-permit FIFO reservations to preserve subsequent A2A and TUI steering, without committing canceled input. Retry only a typed precommit idle/busy transition rejection.