From b6d9f488777395e6e7d906e2855750a42326b02b Mon Sep 17 00:00:00 2001 From: Nikhil Unni Date: Tue, 10 Nov 2026 15:33:26 -0800 Subject: [PATCH] host-agent: the host service decodes requests up to 32 MiB The host service raised only its encode limit, so it decoded requests with tonic's 4 MiB default. A live move's restore request carries the memory manifest of the VM, about 6 MB for 32 GiB of guest RAM. The destination refused it ("decoded message length too large"), each retry failed the same way, and the move rolled back to the source. One constant, HOST_RPC_MAX_MESSAGE_BYTES, now sets the client's decode limit and the server's encode and decode limits. Test: the_host_accepts_a_request_above_the_tonic_default_limit sends a 6 MiB request through the real server and client. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01HNhpmz4QPYaVCE7tFna54D --- crates/engram-host-agent/src/grpc_server.rs | 8 +++++-- .../tests/grpc_pause_resume.rs | 23 +++++++++++++++++-- crates/engram-protocol/src/grpc_client.rs | 15 +++++++----- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/engram-host-agent/src/grpc_server.rs b/crates/engram-host-agent/src/grpc_server.rs index a4546944b..905b0742a 100644 --- a/crates/engram-host-agent/src/grpc_server.rs +++ b/crates/engram-host-agent/src/grpc_server.rs @@ -39,7 +39,7 @@ use engram_protocol::grpc::{ SendHarnessToolResultRequest, SnapshotBeginResponse, SnapshotResponse, StartAgentRequest, UnbindHarnessSessionRequest, }; -use engram_protocol::grpc_client::link_remote_parent; +use engram_protocol::grpc_client::{link_remote_parent, HOST_RPC_MAX_MESSAGE_BYTES}; use engram_protocol::wire::WireExecRequest; use futures::Stream; use std::pin::Pin; @@ -191,7 +191,11 @@ pub async fn serve( .initial_stream_window_size(Some(16 * 1024 * 1024)) .initial_connection_window_size(Some(32 * 1024 * 1024)) .http2_adaptive_window(Some(true)) - .add_service(HostServiceServer::new(svc).max_encoding_message_size(32 * 1024 * 1024)) + .add_service( + HostServiceServer::new(svc) + .max_encoding_message_size(HOST_RPC_MAX_MESSAGE_BYTES) + .max_decoding_message_size(HOST_RPC_MAX_MESSAGE_BYTES), + ) .serve_with_incoming(incoming) .await } diff --git a/crates/engram-host-agent/tests/grpc_pause_resume.rs b/crates/engram-host-agent/tests/grpc_pause_resume.rs index 4951aaf9d..67a8ea051 100644 --- a/crates/engram-host-agent/tests/grpc_pause_resume.rs +++ b/crates/engram-host-agent/tests/grpc_pause_resume.rs @@ -30,6 +30,7 @@ use parking_lot::Mutex; #[derive(Default)] struct FakeHost { calls: Mutex>, + prompt_bytes: Mutex>, } #[async_trait] @@ -100,10 +101,11 @@ impl HostClient for FakeHost { &self, _: SandboxId, _: String, - _: String, + text: String, _: Option, ) -> Result<(), SandboxError> { - unreachable!() + self.prompt_bytes.lock().push(text.len()); + Ok(()) } async fn proxy_shell(&self, _: SandboxId) -> Result { unreachable!() @@ -182,3 +184,20 @@ async fn pause_then_resume_round_trip_to_the_host_with_the_right_sandbox() { "pause/resume did not round-trip in order with the right sandbox id", ); } + +/// A move's restore request carries the VM's memory manifest and is larger +/// than tonic's 4 MiB default decode limit. The host accepts it. +#[tokio::test] +async fn the_host_accepts_a_request_above_the_tonic_default_limit() { + let host = Arc::new(FakeHost::default()); + let addr = boot_grpc_server(host.clone()).await; + let client = connect_grpc_client(addr).await; + + let text = "x".repeat(6 * 1024 * 1024); + client + .send_prompt(SandboxId::new(), "p1".into(), text.clone(), None) + .await + .expect("a 6 MiB request reaches the host"); + + assert_eq!(*host.prompt_bytes.lock(), vec![text.len()]); +} diff --git a/crates/engram-protocol/src/grpc_client.rs b/crates/engram-protocol/src/grpc_client.rs index a6838feaa..2cd7167c9 100644 --- a/crates/engram-protocol/src/grpc_client.rs +++ b/crates/engram-protocol/src/grpc_client.rs @@ -141,6 +141,13 @@ fn restore_rpc_timeout() -> Duration { Duration::from_secs(secs) } +/// The largest `HostService` message either side decodes or encodes. +/// tonic's 4 MiB default is below a move's restore request, which carries +/// the memory manifest of the VM (about 6 MB for 32 GiB of guest RAM), +/// and below a peer chunk frame. The cap is a limit check; it allocates +/// nothing. +pub const HOST_RPC_MAX_MESSAGE_BYTES: usize = 32 * 1024 * 1024; + /// Coord-side client wrapping a `tonic::transport::Channel` to one /// host. Cheap to clone (tonic clients hold an `Arc` /// internally); one instance per host is the steady-state shape. @@ -159,15 +166,11 @@ impl GrpcHostClient { /// Every RPC carries the caller's `traceparent` via /// [`TraceparentInjector`] (ADR 0019 distributed tracing). /// - /// The inbound decode cap is 32 MiB on every client (was an opt-in - /// for `ReapMaterializeDir`'s 16 MiB reply; ADR 0095's - /// `PeerChunkGet` frames — 4 MiB data + overhead — need past - /// tonic's 4 MiB default too, and the cap is just a limit check, - /// no allocation change). + /// The client decodes replies up to [`HOST_RPC_MAX_MESSAGE_BYTES`]. pub fn new(channel: Channel) -> Self { Self { inner: HostServiceClient::with_interceptor(channel, TraceparentInjector) - .max_decoding_message_size(32 * 1024 * 1024), + .max_decoding_message_size(HOST_RPC_MAX_MESSAGE_BYTES), } }