Skip to content

fix(transport): ignore JSON-RPC frames that are not objects - #153

Open
PerryLink wants to merge 1 commit into
agentclientprotocol:mainfrom
PerryLink:fix/ignore-non-object-frames
Open

PerryLink wants to merge 1 commit into
agentclientprotocol:mainfrom
PerryLink:fix/ignore-non-object-frames

Conversation

@PerryLink

Copy link
Copy Markdown

Summary

A line that parses as JSON but is not a JSON object[], 123, "x", or null — currently kills an ACP connection permanently. NdjsonTransport.receive() returns whatever json.loads produced, and Connection._process_message() then calls message.get(...) on it, so the AttributeError escapes _receive_loop(), _disconnect() never runs, and the process dies.

null fails one step earlier and differently: it parses to None, which receive() uses as its EOF signal, so the connection is closed as if the peer had hung up.

Malformed JSON is already tolerated (_transport.py:83-85 logs and skips it), and both web transports already refuse non-object frames (ws/server.py:42-43 returns only dicts; http/server.py:242-245 answers 501 for a batch and 400 otherwise). This makes the stdio path behave the same: ignore a frame that is not an object, and keep the connection alive for the frames behind it.

Related issues

Relates to #62 / #72, which fixed the same failure mode for a different input (an oversized frame) with the rationale that "the connection should degrade gracefully rather than crash."

No issue covers non-object frames — I searched open and closed issues and PRs for isinstance dict, non-object frame, AttributeError, batch JSON-RPC, malformed frame, message.get, null frame.

Testing

Reproduced end-to-end on Windows (Python 3.12.14) by spawning a real run_agent child over spawn_stdio_transport, writing one non-object frame followed by a valid initialize:

frame sent first before after
[] connection dead — AttributeError: 'list' object has no attribute 'get' at connection.py:153 initialize answered
123 connection dead — AttributeError: 'int' ... initialize answered
"x" connection dead — AttributeError: 'str' ... initialize answered
null connection dead — read as EOF initialize answered
this is not json tolerated (control) tolerated
{"jsonrpc":"2.0","method":"noop"} tolerated (control) tolerated

The regression test added here is red without the source change:

$ uv run python -m pytest 'tests/test_connection_recovery.py::test_receive_loop_ignores_frames_that_are_not_json_objects' -q
# without the fix:  1 failed   TypeError: list indices must be integers or slices, not str
# with the fix:     1 passed

Full suite: uv run python -m pytest --doctest-modules -q2 failed, 356 passed, 1 skipped. Both failures pre-exist on main on this machine and are unrelated to this change:

  • tests/test_gen_all.py::test_codegen_check_is_clean_and_read_only — the Windows codegen encoding issue already tracked by fix: make the schema codegen host-independent #151.
  • tests/real_user/test_stdio_limits.py::test_spawn_stdio_transport_custom_limit_handles_large_line — Windows-only CRLF artefact: the child's sys.stdout.write("\n") becomes \r\n, so the line is 71682 bytes and the test's own len(line) == LARGE_LINE_SIZE + 1 assertion fails. Upstream CI is ubuntu-only, so CI does not see it.

I did not run make check end to end.

Docs & screenshots

No user-facing docs change. receive() is already annotated dict[str, Any] | None, which this change makes true.

Checklist

  • Conventional Commit title (e.g. feat:, fix:).
  • Tests cover the change or are not required (explain above).
  • Docs/examples updated when behaviour is user-facing.
  • Schema regenerations (make gen-all) are called out if applicable.

Scope note. WsClientTransport.receive() (src/acp/ws/client.py:56) has the same shape — return json.loads(frame) with no object guard — while the WebSocket server transport does guard. I deliberately did not change it here: I verified the stdio path end to end, but I did not stand up a live WebSocket connection to demonstrate the client path, and I would rather leave that visible than claim a fix I did not test. Say the word and I will add it here or in a follow-up.

This branch has not been deployed

No deployments
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