Skip to content

test: guard numeric ACP initialize protocol version - #318

Merged
pedronauck merged 1 commit into
mainfrom
codex/issue-306-acp-handshake
Aug 5, 2026
Merged

pedronauck merged 1 commit into
mainfrom
codex/issue-306-acp-handshake

Conversation

@pedronauck

@pedronauck pedronauck commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Context

Issue #306 reported that a clean Windows installation of Compozy v0.3.0-beta.3 disconnected while starting @agentclientprotocol/[email protected]. The report attributed the disconnect to Compozy sending initialize.params.protocolVersion as a JSON string after the adapter allegedly changed the field to a number.

That attribution does not reproduce against the released Compozy code or either adapter version named in the report. This PR therefore does not claim a production serialization fix that did not occur. It closes the actual coverage gap: Compozy had no raw-wire assertion preventing a future regression from a numeric ACP version to a quoted value.

Fixes #306.

Reproduction and root-cause investigation

The dependent ACP API requires an integer

The current ACP contract models protocolVersion as an integer. Compozy uses github.com/coder/acp-go-sdk v0.13.5, where:

  • ProtocolVersionNumber is the numeric constant 1;
  • ProtocolVersion is an integer type;
  • InitializeRequest.ProtocolVersion is serialized directly in the JSON-RPC params.

Direct requests to the exact reported adapter confirm that behavior:

Adapter Wire value Result
[email protected] "protocolVersion":"1" JSON-RPC -32602 Invalid params: expected number, received string
[email protected] "protocolVersion":1 successful initialize; agentInfo.version = "0.64.2"
[email protected] "protocolVersion":"1" the same -32602 Invalid params
[email protected] "protocolVersion":1 successful initialize; agentInfo.version = "0.63.0"

Both npm releases declare @agentclientprotocol/sdk 1.3.0. The adapter's changelog between 0.63.0 and 0.64.2 contains no initialize schema transition. The claimed 0.64-only breaking change, and the explanation that pinning 0.63 restored a legacy string contract, are therefore disproven.

The released Compozy binary already sends the number

The v0.3.0-beta.3 tag and current main both construct:

acpsdk.InitializeRequest{
    ProtocolVersion: acpsdk.ProtocolVersionNumber,
    // ...
}

The official compozy_windows_x86_64.zip beta.3 artifact was also inspected with Go build metadata. Its compozy.exe embeds github.com/coder/acp-go-sdk v0.13.5, the same SDK whose request field is numeric.

There is no prior Compozy commit that converted this field from string to number between beta.3 and current main: both ends already use the same numeric SDK contract.

The real current client completes the reported handshake

A temporary live probe exercised the real internal/acp.Driver.Start path with the exact command:

npx --yes @agentclientprotocol/[email protected]

It completed the production launch, initialize, session/new, and session mode negotiation:

  • process launch: 3 ms
  • initialize: 1,374 ms
  • session/new: 3,292 ms
  • set mode: 10 ms
  • total: 4,682 ms
  • returned adapter version: 0.64.2

The probe stopped the driver, observed the ACP connection close, and confirmed that child PID 66217 no longer existed. The temporary probe source was removed after the run; it is not a network-dependent test added to the canonical unit suite.

Why the original disconnect cannot be assigned to the schema

A quoted version produces a normal JSON-RPC invalid-params response. The reported peer disconnected before response instead means the child process closed the transport before replying. Those are distinct failure modes.

The original crash evidence did not retain the child exit code, signal, or stderr tail, so the reason that the Windows npx process exited cannot be reconstructed from #306. The fact that a later run happened to work after installing 0.63.0 changed the installed process/cache state, but does not prove a protocol difference; 0.63.0 enforces the same numeric schema.

Issue #315 owns the general process_exit / peer disconnected diagnostic work, including exit code, signal, and stderr preservation. This PR deliberately avoids conflicting production changes in that area.

Solution design

The canonical ACP Start contract suite now captures the actual JSON-RPC request written by the real Compozy client to a subprocess and inspects the raw initialize.params object.

The new invariant is:

Compozy sends initialize.params.protocolVersion as the JSON integer equal to acpsdk.ProtocolVersionNumber, never as a quoted string.

The assertion unmarshals the raw field into a Go int. A future change to "1" fails at the wire boundary independently of the SDK's Go type. It also compares the value to the SDK's supported version so a stale hard-coded assertion cannot pass after a protocol update.

The existing subprocess capture harness and lifecycle helpers are reused. No duplicate mock, compatibility DTO, fallback, or adapter pin was added.

Changes by surface

  • Runtime / ACP client: no production code change; current serialization is already correct.
  • ACP test harness: reused the existing real stdin/stdout subprocess capture path; no new mock implementation.
  • CLI, HTTP, and UDS: no behavior or contract change.
  • Provider config: the Claude command remains @latest; no temporary 0.63 pin.
  • Web and packages/site: no impact; no user-visible behavior or documentation promise changed.
  • OpenAPI / generated contracts: no impact.
  • Extensibility / official skill: no public capability, hook, tool, resource, bridge, or operator workflow changed.
  • CI / Windows: the canonical test compiles into a Windows x86-64 PE test binary; production Windows cross-build also passes. No broad CI workflow change was needed for this focused coverage fix.

Test placement and invariants

  • Invariant: raw ACP initialize protocol version is JSON integer 1, not a string.
  • Owning layer: ACP client wire contract over the subprocess JSON-RPC transport.
  • Canonical suite: internal/acp/client_start_contract_test.go.
  • Harness: existing request capture in internal/acp/client_test_support_test.go.

No duplicate assertion was added to HTTP, CLI, UDS, web, config, generated output, or the ACP mock-agent implementation because none of those layers owns serialization of the client's initialize request.

Verification and QA evidence

Focused and package verification

  • CGO_ENABLED=1 go test -race ./internal/acp -run '^TestStartInitializeContract$' -count=1 — pass
  • CGO_ENABLED=1 go test -race ./internal/acp/... -count=1 — pass
  • make gate — pass; scoped Go lint reported 0 issues and the ACP race suite passed
  • git diff --check — pass

Windows evidence

  • GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c ./internal/acp — pass
  • compiled artifact identified as PE32+ executable (console) x86-64, for MS Windows
  • temporary Windows test binary removed and absence confirmed
  • make cross-build-windows — pass

Real adapter probe

  • real Compozy Driver.Start against [email protected] — pass through initialize, session creation, and mode negotiation
  • child process teardown confirmed; no live PID remained
  • direct raw 0.63.0 and 0.64.2 adapter probes prove both versions reject strings and accept numbers

Full repository gate

make gate-full passed for fingerprint f856af8997bc750b8bf742e2679ee4880971b1b9.

The recorded full gate includes codegen and installer checks, Bun lint/typecheck/tests, web build, Go format/lint/race tests, Go and Windows builds, and package boundaries. The Go suite completed 20,689 tests with 2 expected platform/helper skips.

QA tracker

This diff changes only internal automated coverage. It does not change a UI, CLI verb, API route, configuration key, public copy, or runtime behavior, so no living QA scenario was added or reset. The live adapter probe is the scenario evidence for the reported handshake, and it was torn down cleanly.

Compozy Impact Audit

  • Native tools: no impact. Checked compozy__* tool IDs, toolsets, descriptors, schemas, digests, risk flags, diagnostics, capability gates, and CLI/API fallbacks; none serialize ACP initialize requests or change in this test-only diff.
  • Extensibility and hooks: no impact. Checked extensions, hooks, skills/capabilities, tools/resources, registries, bridge SDKs, MCP sidecars, provider config lifecycle, and the ACP launch registry. The wire assertion protects the existing internal client contract without adding or changing an extension surface.
  • Workspace data isolation: no impact. The initialize version is transport metadata, not persisted data. No global/workspace/session/agent-scoped datum, workspace_id propagation, store, cache, SSE, or event path changes.
  • Official Compozy skill: no impact. Checked skills/compozy/; no tool ID, CLI path, hook event, capability, extension resource, memory/network/task semantic, or operator workflow changes.

Compatibility, risks, and decisions

  • Compatibility: preserves the stable numeric ACP v1 contract for every supported ACP provider. There is no legacy string compatibility path because the protocol and both reported Claude adapter versions require a number.
  • Dependency posture: keeps claude-agent-acp@latest; no temporary dependency pin or downgrade.
  • Residual risk: the exact historical reason the Windows child exited is unknowable from the available evidence because exit diagnostics were not captured. Codex ACP process exits with peer disconnected before response while streaming final answer #315 owns that cross-provider diagnostic gap.
  • Regression protection: if Compozy ever emits a string, omits the field, or sends a version different from the SDK-supported value, the canonical Start contract suite now fails at the raw JSON boundary.
  • Scope decision: no production line was changed because current main, the beta.3 source, the released Windows binary, and the live 0.64.2 probe all demonstrate correct production behavior.

Summary by CodeRabbit

  • Tests
    • Added contract coverage for ACP initialization to verify that the protocol version is sent in the expected JSON format.
    • Improved confidence that initialization remains compatible with the SDK’s supported protocol version.

@vercel

vercel Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
compozy-site Ready Ready Preview Aug 5, 2026 8:56pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 65f43271-03be-4920-86fc-75949013e432

📥 Commits

Reviewing files that changed from the base of the PR and between 8be5b22 and c303952.

📒 Files selected for processing (1)
  • internal/acp/client_start_contract_test.go

Walkthrough

The change adds an ACP startup contract test. The test decodes the initialize request and verifies that protocolVersion is a JSON number matching acpsdk.ProtocolVersionNumber.

Changes

ACP initialize contract

Layer / File(s) Summary
Validate numeric protocol version
internal/acp/client_start_contract_test.go
The test captures the ACP initialize request, decodes its parameters, and verifies the numeric protocolVersion value.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the added test that guards the numeric ACP initialize protocol version.
Linked Issues check ✅ Passed The test verifies the numeric protocolVersion wire contract required by issue #306 [#306].
Out of Scope Changes check ✅ Passed The pull request only adds an ACP contract test directly related to issue #306 [#306].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-306-acp-handshake

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.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds wire-level regression coverage for the existing ACP initialization contract.

  • Captures the real subprocess JSON-RPC initialize request.
  • Verifies that protocolVersion is encoded as a JSON integer rather than a quoted string.
  • Compares the transmitted value with the ACP SDK’s supported protocol version.

Confidence Score: 5/5

The PR appears safe to merge, with the new test correctly protecting the numeric ACP initialization wire contract.

The change is isolated to test coverage, follows the established subprocess capture lifecycle, and introduces no production runtime or contract changes.

Important Files Changed

Filename Overview
internal/acp/client_start_contract_test.go Adds a focused contract test that reliably rejects missing, quoted, non-integral, or incorrect ACP protocol-version values without changing production behavior.

Reviews (1): Last reviewed commit: "test: guard ACP initialize protocol vers..." | Re-trigger Greptile

This branch was successfully deployed

1 active deployment
Preview — c303952c Deployed Aug 5, 2026 by vercel[bot]
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.

ACP initialize handshake fails: claude-agent-acp v0.64 requires protocolVersion as number, Compozy sends string

1 participant