diff --git a/crates/acp-tunnel/src/lib.rs b/crates/acp-tunnel/src/lib.rs index 139edb8..9a01e4f 100644 --- a/crates/acp-tunnel/src/lib.rs +++ b/crates/acp-tunnel/src/lib.rs @@ -29,6 +29,16 @@ pub mod config; /// The ACP sub-protocol token the server echoes back on the `/acp` upgrade. pub const ACP_SUBPROTOCOL: &str = "acp.v1"; +/// The ACP protocol version Studio negotiates on the `/acp` `initialize`. +/// +/// ACP versions the wire protocol with a **u16 integer** — unlike MCP, which +/// uses a date string (e.g. `"2024-11-05"`). The gateway deserializes this +/// field as `u16`, so sending the MCP string fails with +/// `invalid type: string, expected u16`. The *tunnelled* MCP `initialize` +/// (what Studio relays to the `oab-mcp` sidecar) still uses the MCP date string — +/// only the outer `/acp` handshake uses this integer. +pub const ACP_PROTOCOL_VERSION: u16 = 1; + /// The WebSocket `Sec-WebSocket-Protocol` offer for a bearer-authed `/acp` dial: /// `openab.bearer., acp.v1` (tunnel contract §1). The server echoes only /// `acp.v1`. The token is a secret — this returns it for the transport to place @@ -350,6 +360,17 @@ mod tests { ); } + #[test] + fn acp_protocol_version_is_an_integer_not_an_mcp_date_string() { + // ACP versions the protocol with a u16; MCP uses a date string. The + // `/acp` gateway deserializes `protocolVersion` as u16, so it must + // serialize to a JSON number — a string regresses to + // `invalid type: string, expected u16`. + let v = json!({ "protocolVersion": ACP_PROTOCOL_VERSION }); + assert!(v["protocolVersion"].is_number()); + assert_eq!(v["protocolVersion"], json!(1)); + } + #[test] fn acp_subprotocol_is_a_single_clean_token() { // Native transports offer this verbatim as one `Sec-WebSocket-Protocol` diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs index a59faab..286fe37 100644 --- a/src-tauri/src/remote.rs +++ b/src-tauri/src/remote.rs @@ -164,8 +164,12 @@ async fn run_once( let mut session = Session::new(vec![acp::oab_server(&conn_id)]); // Drive the client handshake: initialize, then session/new declaring "oab". + // This is the outer **ACP** handshake — `protocolVersion` is a u16 integer + // (the gateway deserializes it as `u16`). Do not copy MCP's date string here; + // the MCP date string is correct only for the *tunnelled* inner `initialize` + // in `handle_inner` below. let (_id, init) = session.initialize(json!({ - "protocolVersion": "2024-11-05", + "protocolVersion": acp::ACP_PROTOCOL_VERSION, "clientCapabilities": {}, "clientInfo": { "name": "oab-studio", "version": env!("CARGO_PKG_VERSION") } }));