Allow disabling the in-process code-mode host fallback (#35266)

## What changed

- Allow `features.code_mode_host` to use a configuration table with
  `disable_in_process_fallback`. When enabled, failure to start the standalone
  host is returned as tool output instead of falling back to embedded V8.
- Preserve the existing fallback behavior by default and continue accepting the
  boolean feature toggle.
- Limit displayed host paths in spawn errors to 512 bytes while retaining the
  executable-bearing suffix and valid UTF-8 boundaries.

## Testing

- Cover boolean and table-based feature configuration, fallback-disabled host
  failures, and bounded ASCII and UTF-8 error paths.

GitOrigin-RevId: ab3d014e79054c2f8beef9a658915f01cca197b2
This commit is contained in:
Channing Conger
2026-07-25 00:01:43 +00:00
committed by copyberry
parent a453588416
commit cba0e2701c
14 changed files with 297 additions and 18 deletions
+8 -1
View File
@@ -38,6 +38,7 @@ type ShutdownResultReceiver = watch::Receiver<Option<Result<(), String>>>;
/// Creates code-mode sessions backed by one lazily spawned process host.
pub struct ProcessOwnedCodeModeSessionProvider {
state: StdMutex<ProviderState>,
allow_in_process_fallback: bool,
}
/// Creates code-mode sessions backed by one shared remote WebSocket connection.
@@ -56,9 +57,15 @@ impl ProcessOwnedCodeModeSessionProvider {
state: StdMutex::new(ProviderState::OwnedProcess(Arc::new(
OwnedCodeModeHost::new(host_program),
))),
allow_in_process_fallback: true,
}
}
pub fn without_in_process_fallback(mut self) -> Self {
self.allow_in_process_fallback = false;
self
}
fn process_host(&self) -> Option<Arc<OwnedCodeModeHost>> {
match &*self
.state
@@ -91,7 +98,7 @@ impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
match process_host.connection().await {
Ok(_) => {}
Err(error) if error.host_program_not_found() => {
Err(error) if error.host_program_not_found() && self.allow_in_process_fallback => {
*self
.state
.lock()
@@ -58,6 +58,10 @@ mod transport;
const IPC_CHANNEL_CAPACITY: usize = 128;
const HOST_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
const MAX_WEBSOCKET_FRAME_BYTES: usize = MAX_FRAME_BYTES + std::mem::size_of::<u32>();
// Host spawn errors become model-visible tool output. Bound configured paths
// while preserving the executable-bearing suffix needed to diagnose failures.
const MAX_DISPLAYED_HOST_PROGRAM_BYTES: usize = 512;
const TRUNCATED_HOST_PROGRAM_PREFIX: &str = "...";
pub(super) enum ConnectionError {
Spawn {
@@ -82,11 +86,27 @@ impl fmt::Display for ConnectionError {
Self::Spawn {
host_program,
error,
} => write!(
formatter,
"failed to spawn code-mode host {}: {error}",
host_program.display()
),
} => {
let host_program = host_program.to_string_lossy();
if host_program.len() <= MAX_DISPLAYED_HOST_PROGRAM_BYTES {
return write!(
formatter,
"failed to spawn code-mode host {host_program}: {error}"
);
}
let mut suffix_start = host_program.len()
- (MAX_DISPLAYED_HOST_PROGRAM_BYTES - TRUNCATED_HOST_PROGRAM_PREFIX.len());
while !host_program.is_char_boundary(suffix_start) {
suffix_start += 1;
}
write!(
formatter,
"failed to spawn code-mode host {TRUNCATED_HOST_PROGRAM_PREFIX}{}: {error}",
&host_program[suffix_start..]
)
}
Self::Other(message) => formatter.write_str(message),
}
}
@@ -32,6 +32,7 @@ use tokio_tungstenite::tungstenite::Message;
use super::ProcessOwnedCodeModeSession;
use super::ProcessOwnedCodeModeSessionProvider;
use super::WebSocketCodeModeSessionProvider;
use super::connection::ConnectionError;
use super::resolve_host_program;
use crate::NoopCodeModeSessionDelegate;
@@ -93,6 +94,41 @@ fn host_program_falls_back_to_its_name_when_main_executable_is_unknown() {
);
}
#[test]
fn missing_host_error_limits_the_displayed_path_to_512_bytes() {
let executable = "codex-code-mode-host-does-not-exist";
let host_program = format!("{}{executable}", "missing-directory/".repeat(/*n*/ 64));
let expected_suffix = &host_program[host_program.len() - (512 - "...".len())..];
let error = ConnectionError::Spawn {
host_program: PathBuf::from(&host_program),
error: io::Error::new(io::ErrorKind::NotFound, "host unavailable"),
};
assert_eq!(
error.to_string(),
format!("failed to spawn code-mode host ...{expected_suffix}: host unavailable")
);
}
#[test]
fn missing_host_error_preserves_utf8_boundaries_when_truncating_the_path() {
let executable = "codex-code-mode-host-does-not-exist";
let host_program = format!("{}{executable}", "🦀".repeat(/*n*/ 256));
let error = ConnectionError::Spawn {
host_program: PathBuf::from(host_program),
error: io::Error::new(io::ErrorKind::NotFound, "host unavailable"),
}
.to_string();
let displayed_path = error
.strip_prefix("failed to spawn code-mode host ")
.and_then(|message| message.strip_suffix(": host unavailable"))
.expect("missing-host error should contain the displayed host path");
assert!(displayed_path.starts_with("..."));
assert!(displayed_path.ends_with(executable));
assert!(displayed_path.len() <= 512);
}
#[tokio::test]
async fn provider_falls_back_to_in_process_session_when_host_is_missing() {
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
@@ -273,6 +309,23 @@ async fn websocket_provider_executes_over_shared_connector() {
.expect("websocket test host task should succeed");
}
#[tokio::test]
async fn provider_returns_missing_host_error_when_in_process_fallback_is_disabled() {
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
"codex-code-mode-host-does-not-exist".into(),
)
.without_in_process_fallback();
let error = provider
.create_session(Arc::new(NoopCodeModeSessionDelegate))
.await
.err()
.expect("missing host should fail when in-process fallback is disabled");
assert!(error.contains("failed to spawn code-mode host codex-code-mode-host-does-not-exist"));
assert!(provider.process_host().is_some());
}
#[tokio::test]
async fn shutdown_before_open_does_not_spawn_the_host() {
let session = ProcessOwnedCodeModeSession::new();