Add per-session code-mode execution limits (#37114)

## What changed

- Add `create_session_with_limits` and session-scoped cell execution limits.
- Clamp execute and wait yield times to the session's `max_yield_time_ms`
  without terminating the running cell.
- Negotiate support with remote code-mode hosts and include non-default limits
  in `session/open`, while keeping unlimited sessions compatible with hosts and
  providers that do not support limits.

## Testing

- Cover yield-time clamping, zero-timeout behavior, and isolation between
  sessions.
- Cover wire serialization, capability negotiation, unsupported hosts, and
  shared process-host execution.

GitOrigin-RevId: 9517321cd605bb87f93eeaa6ba331cc2e346e582
This commit is contained in:
Sean Huang
2026-08-05 16:09:01 +00:00
committed by copyberry
parent 2707dfc219
commit 9d00bb01c0
18 changed files with 730 additions and 57 deletions
+46 -5
View File
@@ -8,6 +8,7 @@ use std::sync::atomic::Ordering;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeSession;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::CodeModeSessionProvider;
use codex_code_mode_protocol::CodeModeSessionProviderFuture;
@@ -85,7 +86,15 @@ impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
&'a self,
delegate: Arc<dyn CodeModeSessionDelegate>,
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(create_host_session(delegate, self.process_host()))
self.create_session_with_limits(delegate, CodeModeSessionCellExecutionLimits::default())
}
fn create_session_with_limits<'a>(
&'a self,
delegate: Arc<dyn CodeModeSessionDelegate>,
limits: CodeModeSessionCellExecutionLimits,
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(create_host_session(delegate, self.process_host(), limits))
}
}
@@ -100,6 +109,14 @@ impl CodeModeSessionProvider for DisabledCodeModeSessionProvider {
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(async { Err("code-mode host is disabled".to_string()) })
}
fn create_session_with_limits<'a>(
&'a self,
delegate: Arc<dyn CodeModeSessionDelegate>,
_limits: CodeModeSessionCellExecutionLimits,
) -> CodeModeSessionProviderFuture<'a> {
self.create_session(delegate)
}
}
impl WebSocketCodeModeSessionProvider {
@@ -129,15 +146,28 @@ impl CodeModeSessionProvider for WebSocketCodeModeSessionProvider {
&'a self,
delegate: Arc<dyn CodeModeSessionDelegate>,
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(create_host_session(delegate, Arc::clone(&self.host)))
self.create_session_with_limits(delegate, CodeModeSessionCellExecutionLimits::default())
}
fn create_session_with_limits<'a>(
&'a self,
delegate: Arc<dyn CodeModeSessionDelegate>,
limits: CodeModeSessionCellExecutionLimits,
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(create_host_session(
delegate,
Arc::clone(&self.host),
limits,
))
}
}
async fn create_host_session(
delegate: Arc<dyn CodeModeSessionDelegate>,
host: Arc<OwnedCodeModeHost>,
limits: CodeModeSessionCellExecutionLimits,
) -> Result<Arc<dyn CodeModeSession>, String> {
let session = ProcessOwnedCodeModeSession::with_host(delegate, host);
let session = ProcessOwnedCodeModeSession::with_host(delegate, host, limits);
session.connection().await?;
Ok(Arc::new(session))
}
@@ -244,6 +274,7 @@ struct SessionBinding {
struct SessionInner {
host: Arc<OwnedCodeModeHost>,
delegate: Arc<dyn CodeModeSessionDelegate>,
limits: CodeModeSessionCellExecutionLimits,
state: StdMutex<SessionState>,
next_generation: AtomicU64,
shutdown_requested: AtomicBool,
@@ -263,14 +294,20 @@ impl ProcessOwnedCodeModeSession {
Arc::new(OwnedCodeModeHost::new(
InstallContext::current().code_mode_host_program(),
)),
CodeModeSessionCellExecutionLimits::default(),
)
}
fn with_host(delegate: Arc<dyn CodeModeSessionDelegate>, host: Arc<OwnedCodeModeHost>) -> Self {
fn with_host(
delegate: Arc<dyn CodeModeSessionDelegate>,
host: Arc<OwnedCodeModeHost>,
limits: CodeModeSessionCellExecutionLimits,
) -> Self {
Self {
inner: Arc::new(SessionInner {
host,
delegate,
limits,
state: StdMutex::new(SessionState::New),
next_generation: AtomicU64::new(1),
shutdown_requested: AtomicBool::new(false),
@@ -361,7 +398,11 @@ impl SessionInner {
let result = match self.host.connection().await {
Ok(connection) => {
let cleanup = connection
.open_session(remote.clone(), Arc::clone(&self.delegate))
.open_session(
remote.clone(),
Arc::clone(&self.delegate),
self.limits.clone(),
)
.await;
cleanup.map(|cleanup| SessionBinding {
connection,
@@ -10,6 +10,7 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::StartedCell;
@@ -28,6 +29,7 @@ use codex_code_mode_protocol::host::MAX_FRAME_BYTES;
use codex_code_mode_protocol::host::MAX_PENDING_DELEGATE_CALLS;
use codex_code_mode_protocol::host::ProtocolVersion;
use codex_code_mode_protocol::host::RequestId;
use codex_code_mode_protocol::host::SESSION_RESOURCE_LIMITS_CAPABILITY;
use codex_code_mode_protocol::host::SupportedProtocolVersions;
use codex_code_mode_protocol::host::TransportLane;
use codex_http_client::HttpClientFactory;
@@ -120,6 +122,7 @@ pub(super) struct Connection {
alive: Arc<AtomicBool>,
failure: Arc<std::sync::Mutex<Option<String>>>,
cancellation: CancellationToken,
capabilities: CapabilitySet,
}
struct CallerCancellation {
@@ -285,11 +288,14 @@ impl Connection {
let handshake = async {
let dual_capability =
Capability::new(DUAL_WEBSOCKET_CAPABILITY).map_err(|error| error.to_string())?;
let session_limits_capability = Capability::new(SESSION_RESOURCE_LIMITS_CAPABILITY)
.map_err(|error| error.to_string())?;
let optional_capabilities = if bulk_connection_options.is_some() {
CapabilitySet::try_new([dual_capability.clone()])
CapabilitySet::try_new([dual_capability.clone(), session_limits_capability])
.map_err(|error| error.to_string())?
} else {
CapabilitySet::empty()
CapabilitySet::try_new([session_limits_capability])
.map_err(|error| error.to_string())?
};
let hello = ClientHello::new(
SupportedProtocolVersions::try_new([ProtocolVersion::V1])
@@ -310,7 +316,8 @@ impl Connection {
Some(HostToClient::HostHello(hello))
if hello.selected_version() == ProtocolVersion::V1 =>
{
if hello.capabilities().contains(&dual_capability) {
let capabilities = hello.capabilities().clone();
let bulk_token = if capabilities.contains(&dual_capability) {
hello
.bulk_connection_token()
.map(str::to_string)
@@ -318,12 +325,15 @@ impl Connection {
"code-mode host advertised dual websockets without a pairing token"
.to_string()
})
.map(Some)
.map(Some)?
} else if hello.bulk_connection_token().is_some() {
Err("code-mode host returned an unexpected bulk pairing token".to_string())
return Err(
"code-mode host returned an unexpected bulk pairing token".to_string()
);
} else {
Ok(None)
}
None
};
Ok((capabilities, bulk_token))
}
Some(HostToClient::HandshakeRejected { reason }) => {
Err(format!("code-mode host rejected the handshake: {reason:?}"))
@@ -344,8 +354,8 @@ impl Connection {
));
}
};
let bulk_token = match handshake_result {
Ok(token) => token,
let (capabilities, bulk_token) = match handshake_result {
Ok(negotiated) => negotiated,
Err(err) => {
let _ = writer.close().await;
owner.close().await;
@@ -460,6 +470,7 @@ impl Connection {
alive,
failure,
cancellation,
capabilities,
})
}
@@ -478,13 +489,25 @@ impl Connection {
&self,
session: RemoteSession,
delegate: Arc<dyn CodeModeSessionDelegate>,
limits: CodeModeSessionCellExecutionLimits,
) -> Result<SessionCleanup, String> {
if limits != CodeModeSessionCellExecutionLimits::default()
&& !self
.capabilities
.iter()
.any(|capability| capability.as_str() == SESSION_RESOURCE_LIMITS_CAPABILITY)
{
return Err(format!(
"code-mode host does not support session resource limits: missing `{SESSION_RESOURCE_LIMITS_CAPABILITY}` capability"
));
}
let cleanup = SessionCleanup::new();
let cancellation = CallerCancellation::new();
let (response_tx, response_rx) = oneshot::channel();
self.send(DriverCommand::OpenSession {
session,
delegate,
limits,
cleanup: cleanup.clone(),
caller_cancellation: cancellation.token(),
response_tx,
@@ -1,6 +1,7 @@
use std::sync::Arc;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::WaitOutcome;
@@ -8,6 +9,7 @@ use codex_code_mode_protocol::WaitRequest;
use codex_code_mode_protocol::host::ClientToHost;
use codex_code_mode_protocol::host::EncodedFrame;
use codex_code_mode_protocol::host::HostRequest;
use codex_code_mode_protocol::host::WireSessionCellExecutionLimits;
use codex_code_mode_protocol::host::WireWaitRequest;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
@@ -28,10 +30,18 @@ impl ConnectionDriver {
DriverCommand::OpenSession {
session,
delegate,
limits,
cleanup,
caller_cancellation,
response_tx,
} => self.open_session(session, delegate, cleanup, caller_cancellation, response_tx),
} => self.open_session(
session,
delegate,
limits,
cleanup,
caller_cancellation,
response_tx,
),
DriverCommand::Execute {
session,
request,
@@ -60,6 +70,7 @@ impl ConnectionDriver {
&mut self,
session: RemoteSession,
delegate: Arc<dyn CodeModeSessionDelegate>,
limits: CodeModeSessionCellExecutionLimits,
cleanup: super::cleanup::SessionCleanup,
caller_cancellation: CancellationToken,
response_tx: oneshot::Sender<Result<(), String>>,
@@ -71,6 +82,15 @@ impl ConnectionDriver {
)));
return true;
}
let limits = match WireSessionCellExecutionLimits::try_from(limits) {
Ok(limits) => limits,
Err(error) => {
let _ = response_tx.send(Err(format!(
"failed to encode code-mode session execution limits: {error}"
)));
return true;
}
};
let request_id = match self.requests.allocate_id() {
Ok(id) => id,
Err(err) => {
@@ -82,6 +102,8 @@ impl ConnectionDriver {
id: request_id,
request: HostRequest::OpenSession {
session_id: session.id.clone(),
cell_execution_limits: (limits != WireSessionCellExecutionLimits::default())
.then_some(limits),
},
};
let frame = match EncodedFrame::encode(&message) {
@@ -1,6 +1,7 @@
use std::sync::Arc;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::RuntimeResponse;
@@ -30,6 +31,7 @@ pub(in crate::remote_session::connection) enum DriverCommand {
OpenSession {
session: RemoteSession,
delegate: Arc<dyn CodeModeSessionDelegate>,
limits: CodeModeSessionCellExecutionLimits,
cleanup: SessionCleanup,
caller_cancellation: CancellationToken,
response_tx: oneshot::Sender<Result<(), String>>,
@@ -7,16 +7,19 @@ use std::time::Duration;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeNestedToolCall;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::NotificationFuture;
use codex_code_mode_protocol::ToolInvocationFuture;
use codex_code_mode_protocol::WaitRequest;
use codex_code_mode_protocol::host::CapabilitySet;
use codex_code_mode_protocol::host::ClientToHost;
use codex_code_mode_protocol::host::DelegateRequest;
use codex_code_mode_protocol::host::DelegateRequestId;
use codex_code_mode_protocol::host::DelegateResponse;
use codex_code_mode_protocol::host::EncodedFrame;
use codex_code_mode_protocol::host::HostRequest;
use codex_code_mode_protocol::host::HostResponse;
use codex_code_mode_protocol::host::HostToClient;
use codex_code_mode_protocol::host::MAX_PENDING_DELEGATE_CALLS;
@@ -25,6 +28,7 @@ use codex_code_mode_protocol::host::SessionId;
use codex_code_mode_protocol::host::WireNestedToolCall;
use codex_code_mode_protocol::host::WireResult;
use codex_code_mode_protocol::host::WireRuntimeResponse;
use codex_code_mode_protocol::host::WireSessionCellExecutionLimits;
use codex_code_mode_protocol::host::WireWaitOutcome;
use codex_protocol::ToolName;
use pretty_assertions::assert_eq;
@@ -95,6 +99,7 @@ impl DriverHarness {
.send(DriverCommand::OpenSession {
session: session.clone(),
delegate,
limits: Default::default(),
cleanup: cleanup.clone(),
caller_cancellation: CancellationToken::new(),
response_tx,
@@ -190,6 +195,50 @@ impl Drop for DriverHarness {
}
}
#[tokio::test]
async fn open_session_includes_nondefault_cell_execution_limits() {
let mut harness = DriverHarness::start();
let session = remote_session();
let limits = CodeModeSessionCellExecutionLimits {
max_yield_time_ms: Some(250),
max_heap_size_bytes: Some(16 * 1024 * 1024),
};
let (response_tx, _response_rx) = oneshot::channel();
harness
.command_tx
.send(DriverCommand::OpenSession {
session: session.clone(),
delegate: Arc::new(RecordingDelegate::default()),
limits,
cleanup: SessionCleanup::new(),
caller_cancellation: CancellationToken::new(),
response_tx,
})
.await
.expect("limited session open command");
let frame = harness
.outgoing_rx
.recv()
.await
.expect("limited session open frame");
assert_eq!(
EncodedFrame::decode_framed::<ClientToHost>(&frame.into_framed_bytes())
.expect("decode limited session open request"),
ClientToHost::Request {
id: RequestId::new(/*value*/ 1),
request: HostRequest::OpenSession {
session_id: session.id,
cell_execution_limits: Some(WireSessionCellExecutionLimits {
max_yield_time_ms: Some(250),
max_heap_size_bytes: Some(16 * 1024 * 1024),
}),
},
}
);
}
#[derive(Default)]
struct RecordingDelegate {
closed_cells: StdMutex<Vec<CellId>>,
@@ -518,6 +567,7 @@ async fn dropped_open_waiter_shuts_down_committed_session() {
.send(DriverCommand::OpenSession {
session: session.clone(),
delegate: Arc::new(RecordingDelegate::default()),
limits: Default::default(),
cleanup,
caller_cancellation: CancellationToken::new(),
response_tx: open_tx,
@@ -1352,6 +1402,7 @@ async fn queued_remote_wait_times_out_and_invalidates_the_connection() {
alive: Arc::clone(&harness.alive),
failure: Arc::clone(&harness.failure),
cancellation: harness.cancellation.clone(),
capabilities: CapabilitySet::empty(),
};
let response = tokio::spawn(async move {
let result = connection
@@ -1397,6 +1448,7 @@ async fn queued_remote_termination_times_out_and_invalidates_the_connection() {
alive: Arc::clone(&harness.alive),
failure: Arc::clone(&harness.failure),
cancellation: harness.cancellation.clone(),
capabilities: CapabilitySet::empty(),
};
let response = tokio::spawn(async move {
let result = connection
+46 -5
View File
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionProvider;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::FunctionCallOutputContentItem;
@@ -17,6 +18,7 @@ use codex_code_mode_protocol::host::HostRequest;
use codex_code_mode_protocol::host::HostResponse;
use codex_code_mode_protocol::host::HostToClient;
use codex_code_mode_protocol::host::ProtocolVersion;
use codex_code_mode_protocol::host::SESSION_RESOURCE_LIMITS_CAPABILITY;
use codex_code_mode_protocol::host::WireCellId;
use codex_code_mode_protocol::host::WireContentItem;
use codex_code_mode_protocol::host::WireResult;
@@ -117,13 +119,19 @@ async fn websocket_provider_executes_over_shared_connector() {
let request = EncodedFrame::decode_framed::<ClientToHost>(&frame)
.expect("websocket test host should decode a framed protocol message");
let responses = match request {
ClientToHost::ClientHello(_) => vec![HostToClient::HostHello(HostHello::new(
ProtocolVersion::V1,
CapabilitySet::empty(),
))],
ClientToHost::ClientHello(hello) => {
let capability = Capability::new(SESSION_RESOURCE_LIMITS_CAPABILITY)
.expect("session-limit capability");
assert!(hello.optional_capabilities().contains(&capability));
assert_eq!(hello.required_capabilities(), &CapabilitySet::empty());
vec![HostToClient::HostHello(HostHello::new(
ProtocolVersion::V1,
CapabilitySet::empty(),
))]
}
ClientToHost::Request {
id,
request: HostRequest::OpenSession { session_id },
request: HostRequest::OpenSession { session_id, .. },
} => vec![HostToClient::Response {
id,
result: WireResult::Ok {
@@ -192,6 +200,32 @@ async fn websocket_provider_executes_over_shared_connector() {
.create_session(Arc::new(NoopCodeModeSessionDelegate))
.await
.expect("shared websocket connector should open a code-mode session");
let error = provider
.create_session_with_limits(
Arc::new(NoopCodeModeSessionDelegate),
CodeModeSessionCellExecutionLimits {
max_yield_time_ms: Some(250),
max_heap_size_bytes: None,
},
)
.await
.err()
.expect("legacy host should reject a limited session");
assert_eq!(
error,
format!(
"code-mode host does not support session resource limits: missing `{SESSION_RESOURCE_LIMITS_CAPABILITY}` capability"
)
);
let second_session = provider
.create_session(Arc::new(NoopCodeModeSessionDelegate))
.await
.expect("rejecting limited sessions should preserve the shared legacy-host connection");
second_session
.shutdown()
.await
.expect("second unlimited session should shut down");
drop(second_session);
let response = session
.execute(ExecuteRequest {
tool_call_id: "shared-websocket".to_string(),
@@ -261,6 +295,13 @@ async fn websocket_provider_fails_when_a_negotiated_bulk_connection_is_unavailab
let capability =
Capability::new(DUAL_WEBSOCKET_CAPABILITY).expect("dual websocket capability");
assert!(hello.optional_capabilities().contains(&capability));
let session_limits_capability =
Capability::new(SESSION_RESOURCE_LIMITS_CAPABILITY).expect("session-limit capability");
assert!(
hello
.optional_capabilities()
.contains(&session_limits_capability)
);
let hello = HostToClient::HostHello(
HostHello::new(
ProtocolVersion::V1,