Skip to content

fix(a2a)!: admit messages into the interactive session - #269

Open
danielkov wants to merge 3 commits into
mainfrom
fix/a2a-session-admission
Open

danielkov wants to merge 3 commits into
mainfrom
fix/a2a-session-admission

Conversation

@danielkov

Copy link
Copy Markdown
Contributor

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: true are rejected; eventual A2A answer delivery is not included. Standalone --no-stdio and 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.

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.

@kit-code-agent kit-code-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Session-admission POST handling bypasses the existing HTTP media-type/version validation and request-body deadline, permitting browser-origin prompt admission on unauthenticated listeners and indefinite resource retention by incomplete requests.

Comment thread src/protocols/a2a.rs
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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>

Comment thread src/protocols/a2a.rs
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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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>

@kit-code-agent kit-code-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

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