1use std::{collections::HashMap, env, future::Future, pin::Pin, time::Duration};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7 CompatibilityReport, Error, SessionOptions, TextTurnResult, DEFAULT_EVENTS_CHANNEL_CAPACITY,
8};
9
10pub type RestResult<T> = std::result::Result<T, RestError>;
11pub type RestFuture<T> = Pin<Box<dyn Future<Output = RestResult<T>> + Send + 'static>>;
12
13#[derive(Clone, Debug, Default)]
19pub struct RestRouterOptions {
20 pub enable_text_turn_route: bool,
21 pub enable_bridge_routes: bool,
22 pub allow_unsafe_client_options: bool,
23 pub limits: RestLimits,
24}
25
26impl RestRouterOptions {
27 pub fn text_turn() -> Self {
29 Self {
30 enable_text_turn_route: true,
31 enable_bridge_routes: false,
32 allow_unsafe_client_options: false,
33 limits: RestLimits::default(),
34 }
35 }
36
37 pub fn trusted_bridge() -> Self {
39 Self {
40 enable_text_turn_route: true,
41 enable_bridge_routes: true,
42 allow_unsafe_client_options: false,
43 limits: RestLimits::default(),
44 }
45 }
46
47 pub fn with_unsafe_client_options(mut self, allow: bool) -> Self {
52 self.allow_unsafe_client_options = allow;
53 self
54 }
55
56 pub fn with_max_sessions(mut self, max_sessions: usize) -> Self {
57 self.limits.max_sessions = max_sessions;
58 self
59 }
60
61 pub fn with_max_poll_timeout_ms(mut self, timeout_ms: u64) -> Self {
62 self.limits.max_poll_timeout = Duration::from_millis(timeout_ms);
63 self
64 }
65
66 pub fn with_max_text_turn_duration_ms(mut self, timeout_ms: u64) -> Self {
67 self.limits.max_text_turn_duration = Duration::from_millis(timeout_ms);
68 self
69 }
70
71 pub fn with_max_text_turn_output_bytes(mut self, max_bytes: usize) -> Self {
72 self.limits.max_text_turn_output_bytes = max_bytes;
73 self
74 }
75
76 pub fn with_sse_keep_alive_interval_ms(mut self, interval_ms: u64) -> Self {
80 self.limits.sse_keep_alive_interval = Duration::from_millis(interval_ms);
81 self
82 }
83
84 pub fn with_limits(mut self, limits: RestLimits) -> Self {
93 self.limits = limits;
94 self
95 }
96}
97
98#[derive(Clone, Debug)]
127pub struct RestLimits {
128 pub max_sessions: usize,
134 pub max_one_shot_concurrency: usize,
139 pub max_session_call_concurrency: usize,
143 pub max_session_call_concurrency_per_session: usize,
148 pub max_poll_timeout: Duration,
154 pub min_stream_poll_timeout: Duration,
167 pub max_text_turn_duration: Duration,
173 pub max_text_turn_output_bytes: usize,
182 pub max_request_body_bytes: usize,
195 pub pending_request_ttl: Duration,
203 pub max_pending_requests_per_session: usize,
210 pub events_channel_capacity: usize,
226 pub idle_session_ttl: Duration,
232 pub compatibility_ttl: Duration,
237 pub sse_keep_alive_interval: Duration,
246}
247
248impl Default for RestLimits {
249 fn default() -> Self {
250 Self {
251 max_sessions: 16,
252 max_one_shot_concurrency: 4,
253 max_session_call_concurrency: 64,
254 max_session_call_concurrency_per_session: 8,
255 max_poll_timeout: Duration::from_secs(30),
256 min_stream_poll_timeout: Duration::from_millis(250),
257 max_text_turn_duration: Duration::from_secs(10 * 60),
258 max_text_turn_output_bytes: 1024 * 1024,
259 max_request_body_bytes: 2 * 1024 * 1024,
260 pending_request_ttl: Duration::from_secs(600),
261 max_pending_requests_per_session: 64,
262 events_channel_capacity: DEFAULT_EVENTS_CHANNEL_CAPACITY,
263 idle_session_ttl: Duration::from_secs(30 * 60),
264 compatibility_ttl: Duration::from_secs(30),
265 sse_keep_alive_interval: Duration::from_secs(15),
266 }
267 }
268}
269
270#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
278#[error("environment variable `{var}` has an invalid value `{value}`: expected {expected}")]
279pub struct RestLimitsEnvError {
280 pub var: &'static str,
281 pub value: String,
282 pub expected: &'static str,
283}
284
285impl RestLimits {
286 pub fn from_env() -> Self {
300 match Self::try_from_env() {
301 Ok(limits) => limits,
302 Err(error) => panic!("{error}"),
303 }
304 }
305
306 pub fn try_from_env() -> Result<Self, RestLimitsEnvError> {
311 let default = Self::default();
312 Ok(Self {
313 max_sessions: env_usize("CODEX_APP_SERVER_REST_MAX_SESSIONS", default.max_sessions)?,
314 max_one_shot_concurrency: env_usize(
315 "CODEX_APP_SERVER_REST_MAX_ONE_SHOT_CONCURRENCY",
316 default.max_one_shot_concurrency,
317 )?,
318 max_session_call_concurrency: env_usize(
319 "CODEX_APP_SERVER_REST_MAX_SESSION_CALL_CONCURRENCY",
320 default.max_session_call_concurrency,
321 )?,
322 max_session_call_concurrency_per_session: env_usize(
323 "CODEX_APP_SERVER_REST_MAX_SESSION_CALL_CONCURRENCY_PER_SESSION",
324 default.max_session_call_concurrency_per_session,
325 )?,
326 max_poll_timeout: env_duration_ms(
327 "CODEX_APP_SERVER_REST_MAX_POLL_TIMEOUT_MS",
328 default.max_poll_timeout,
329 )?,
330 min_stream_poll_timeout: env_duration_ms(
331 "CODEX_APP_SERVER_REST_MIN_STREAM_POLL_TIMEOUT_MS",
332 default.min_stream_poll_timeout,
333 )?,
334 max_text_turn_duration: env_duration_ms(
335 "CODEX_APP_SERVER_REST_MAX_TEXT_TURN_DURATION_MS",
336 default.max_text_turn_duration,
337 )?,
338 max_text_turn_output_bytes: env_usize(
339 "CODEX_APP_SERVER_REST_MAX_TEXT_TURN_OUTPUT_BYTES",
340 default.max_text_turn_output_bytes,
341 )?,
342 max_request_body_bytes: env_nonzero_usize(
343 "CODEX_APP_SERVER_REST_MAX_REQUEST_BODY_BYTES",
344 default.max_request_body_bytes,
345 )?,
346 pending_request_ttl: env_duration_ms(
347 "CODEX_APP_SERVER_REST_PENDING_REQUEST_TTL_MS",
348 default.pending_request_ttl,
349 )?,
350 max_pending_requests_per_session: env_usize(
351 "CODEX_APP_SERVER_REST_MAX_PENDING_REQUESTS_PER_SESSION",
352 default.max_pending_requests_per_session,
353 )?,
354 events_channel_capacity: env_nonzero_usize(
355 "CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY",
356 default.events_channel_capacity,
357 )?,
358 idle_session_ttl: env_duration_ms(
359 "CODEX_APP_SERVER_REST_IDLE_SESSION_TTL_MS",
360 default.idle_session_ttl,
361 )?,
362 compatibility_ttl: env_duration_ms(
363 "CODEX_APP_SERVER_REST_COMPATIBILITY_TTL_MS",
364 default.compatibility_ttl,
365 )?,
366 sse_keep_alive_interval: env_duration_ms(
367 "CODEX_APP_SERVER_REST_SSE_KEEP_ALIVE_MS",
368 default.sse_keep_alive_interval,
369 )?,
370 })
371 }
372}
373
374fn env_nonzero_usize(var: &'static str, default: usize) -> Result<usize, RestLimitsEnvError> {
388 let value = env_usize(var, default)?;
389 if value == 0 {
390 return Err(RestLimitsEnvError {
391 var,
392 value: "0".to_owned(),
393 expected: "a positive integer (a zero-capacity channel is not constructible)",
394 });
395 }
396 Ok(value)
397}
398
399fn env_usize(var: &'static str, default: usize) -> Result<usize, RestLimitsEnvError> {
400 match env::var(var) {
401 Ok(value) => value
402 .trim()
403 .parse::<usize>()
404 .map_err(|_| RestLimitsEnvError {
405 var,
406 value,
407 expected: "a non-negative integer",
408 }),
409 Err(env::VarError::NotPresent) => Ok(default),
410 Err(env::VarError::NotUnicode(raw)) => Err(RestLimitsEnvError {
411 var,
412 value: raw.to_string_lossy().into_owned(),
413 expected: "valid UTF-8 text",
414 }),
415 }
416}
417
418fn env_duration_ms(var: &'static str, default: Duration) -> Result<Duration, RestLimitsEnvError> {
422 match env::var(var) {
423 Ok(value) => value
424 .trim()
425 .parse::<u64>()
426 .map(Duration::from_millis)
427 .map_err(|_| RestLimitsEnvError {
428 var,
429 value,
430 expected: "a non-negative integer count of milliseconds",
431 }),
432 Err(env::VarError::NotPresent) => Ok(default),
433 Err(env::VarError::NotUnicode(raw)) => Err(RestLimitsEnvError {
434 var,
435 value: raw.to_string_lossy().into_owned(),
436 expected: "valid UTF-8 text",
437 }),
438 }
439}
440
441#[derive(Debug, thiserror::Error)]
443pub enum RestError {
444 #[error("{0}")]
445 NotFound(String),
446
447 #[error("{0}")]
448 Gone(String),
449
450 #[error("{0}")]
451 Forbidden(String),
452
453 #[error("{0}")]
454 InvalidRequest(String),
455
456 #[error("{0}")]
457 RateLimited(String),
458
459 #[error("{0}")]
460 Conflict(String),
461
462 #[error("{0}")]
463 TimedOut(String),
464
465 #[error("{0}")]
466 PayloadTooLarge(String),
467
468 #[error("{0}")]
469 Internal(String),
470
471 #[error(transparent)]
472 Client(#[from] Error),
473}
474
475impl From<serde_json::Error> for RestError {
476 fn from(error: serde_json::Error) -> Self {
477 Self::Client(error.into())
478 }
479}
480
481pub trait RestBackend: Send + Sync + 'static {
487 fn compatibility_report(&self) -> RestFuture<CompatibilityReport>;
488 fn run_text_turn(&self, request: RestTextTurnRequest) -> RestFuture<RestTextTurnResponse>;
489 fn create_session(
490 &self,
491 _request: RestSessionCreateRequest,
492 ) -> RestFuture<RestSessionCreateResponse> {
493 Box::pin(async move {
494 Err(RestError::NotFound(
495 "REST session bridge is not implemented by this backend".to_owned(),
496 ))
497 })
498 }
499 fn list_sessions(&self) -> RestFuture<Vec<RestSessionSummary>> {
500 Box::pin(async move { Ok(Vec::new()) })
501 }
502 fn delete_session(&self, session_id: String) -> RestFuture<RestStatusResponse> {
503 Box::pin(async move {
504 Err(RestError::NotFound(format!(
505 "session `{session_id}` was not found"
506 )))
507 })
508 }
509 fn call_method(&self, request: RestCallRequest) -> RestFuture<RestCallResponse> {
510 Box::pin(async move {
511 Err(RestError::NotFound(format!(
512 "REST raw call `{}` is not implemented by this backend",
513 request.method
514 )))
515 })
516 }
517 fn poll_event(
518 &self,
519 session_id: String,
520 timeout_ms: Option<u64>,
521 ) -> RestFuture<RestEventResponse> {
522 let _ = timeout_ms;
523 Box::pin(async move {
524 Err(RestError::NotFound(format!(
525 "session `{session_id}` was not found"
526 )))
527 })
528 }
529 fn reply_request_result(
530 &self,
531 session_id: String,
532 request_key: String,
533 body: RestRequestReplyResultRequest,
534 ) -> RestFuture<RestRequestReplyResponse> {
535 let _ = (request_key, body);
536 Box::pin(async move {
537 Err(RestError::NotFound(format!(
538 "session `{session_id}` was not found"
539 )))
540 })
541 }
542 fn reply_request_error(
543 &self,
544 session_id: String,
545 request_key: String,
546 body: RestErrorReplyRequest,
547 ) -> RestFuture<RestRequestReplyResponse> {
548 let _ = (request_key, body);
549 Box::pin(async move {
550 Err(RestError::NotFound(format!(
551 "session `{session_id}` was not found"
552 )))
553 })
554 }
555}
556
557#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
559pub struct RestHealthResponse {
560 pub status: String,
561}
562
563#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
565#[serde(rename_all = "snake_case")]
566pub enum RestApprovalPolicy {
567 #[default]
568 DenyAll,
569 ReadOnly,
570 AllowAll,
571}
572
573#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
575#[serde(rename_all = "camelCase", deny_unknown_fields)]
576pub struct RestClientOptions {
577 pub name: Option<String>,
578 pub version: Option<String>,
579 pub command: Option<String>,
580 #[serde(default, skip_serializing_if = "Vec::is_empty")]
581 pub extra_args: Vec<String>,
582 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
583 pub config: HashMap<String, String>,
584 pub call_timeout_ms: Option<u64>,
585}
586
587impl RestClientOptions {
588 pub(super) fn into_session_options(self, default_name: &str) -> SessionOptions {
589 let mut options = SessionOptions::new(
590 self.name.unwrap_or_else(|| default_name.to_owned()),
591 self.version
592 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_owned()),
593 );
594 if let Some(command) = self.command {
595 options = options.with_command(command);
596 }
597 for arg in self.extra_args {
598 options = options.with_extra_arg(arg);
599 }
600 for (key, value) in self.config {
601 options = options.with_config(key, value);
602 }
603 if let Some(timeout_ms) = self.call_timeout_ms {
604 options = options.with_call_timeout(Duration::from_millis(timeout_ms));
605 }
606 options
607 }
608}
609
610#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
612#[serde(rename_all = "camelCase", deny_unknown_fields)]
613pub struct RestTextTurnRequest {
614 pub prompt: String,
615 pub model: Option<String>,
616 pub approval_policy: Option<RestApprovalPolicy>,
617 pub client: Option<RestClientOptions>,
618}
619
620impl RestTextTurnRequest {
621 pub fn session_options(&self) -> SessionOptions {
622 session_options_from(self.client.clone(), "codex_app_server_rest")
623 }
624}
625
626#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
628#[serde(rename_all = "camelCase")]
629pub struct RestTextTurnResponse {
630 pub thread_id: String,
631 pub turn_id: String,
632 pub turn_status: Option<String>,
633 pub agent_message: String,
634 pub latest_diff: Option<String>,
635 pub errors: Vec<Value>,
636}
637
638impl From<TextTurnResult> for RestTextTurnResponse {
639 fn from(result: TextTurnResult) -> Self {
640 let agent_message = result.agent_message().to_owned();
641 let latest_diff = result.latest_diff().map(str::to_owned);
642 let turn_status = result.events.terminal_status().and_then(|status| {
643 serde_json::to_value(status)
644 .ok()?
645 .as_str()
646 .map(str::to_owned)
647 });
648 let errors = result
649 .errors()
650 .iter()
651 .map(|error| {
652 serde_json::to_value(error).unwrap_or_else(|_| Value::String(error.message.clone()))
653 })
654 .collect();
655 Self {
656 thread_id: result.thread.thread.id,
657 turn_id: result.turn.turn.id,
658 turn_status,
659 agent_message,
660 latest_diff,
661 errors,
662 }
663 }
664}
665
666#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
668#[serde(rename_all = "camelCase", deny_unknown_fields)]
669pub struct RestCallBody {
670 #[serde(default)]
671 pub params: Value,
672 pub client: Option<RestClientOptions>,
673}
674
675#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
677#[serde(rename_all = "camelCase")]
678pub struct RestCallRequest {
679 pub session_id: Option<String>,
680 pub method: String,
681 pub params: Value,
682 pub client: Option<RestClientOptions>,
683}
684
685#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
687#[serde(rename_all = "camelCase")]
688pub struct RestCallResponse {
689 pub method: String,
690 pub result: Value,
691}
692
693#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
695#[serde(rename_all = "camelCase", deny_unknown_fields)]
696pub struct RestSessionCreateRequest {
697 pub client: Option<RestClientOptions>,
698}
699
700#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
702#[serde(rename_all = "camelCase")]
703pub struct RestSessionCreateResponse {
704 pub session_id: String,
705 pub initialize_response: Value,
706}
707
708#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
710#[serde(rename_all = "camelCase")]
711pub struct RestSessionSummary {
712 pub session_id: String,
713}
714
715#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
717pub struct RestListSessionsResponse {
718 pub sessions: Vec<RestSessionSummary>,
719}
720
721#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
723pub struct RestStatusResponse {
724 pub status: String,
725}
726
727#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
729#[serde(
730 tag = "event",
731 rename_all = "snake_case",
732 rename_all_fields = "camelCase"
733)]
734pub enum RestEventResponse {
735 Notification {
736 notification: Value,
737 },
738 Request {
739 request_key: String,
740 request_id: Value,
741 method: String,
742 request: Value,
743 },
744 Closed,
745 Timeout,
746}
747
748impl RestEventResponse {
749 pub fn notification(notification: Value) -> Self {
750 Self::Notification { notification }
751 }
752
753 pub fn request(
754 request_key: impl Into<String>,
755 request_id: Value,
756 method: impl Into<String>,
757 request: Value,
758 ) -> Self {
759 Self::Request {
760 request_key: request_key.into(),
761 request_id,
762 method: method.into(),
763 request,
764 }
765 }
766
767 pub fn closed() -> Self {
768 Self::Closed
769 }
770
771 pub fn timeout() -> Self {
772 Self::Timeout
773 }
774}
775
776#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
778#[serde(rename_all = "camelCase", deny_unknown_fields)]
779pub struct RestRequestReplyResultRequest {
780 pub result: Value,
781}
782
783#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
785#[serde(rename_all = "camelCase", deny_unknown_fields)]
786pub struct RestErrorReplyRequest {
787 pub code: i64,
788 pub message: String,
789 pub data: Option<Value>,
790}
791
792#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
794pub struct RestRequestReplyResponse {
795 pub status: String,
796}
797
798#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
800pub struct RestErrorResponse {
801 pub error: String,
802 pub message: String,
803 #[serde(skip_serializing_if = "Option::is_none")]
804 pub code: Option<i64>,
805 #[serde(skip_serializing_if = "Option::is_none")]
806 pub data: Option<Value>,
807}
808
809pub(super) fn session_options_from(
810 client: Option<RestClientOptions>,
811 default_name: &str,
812) -> SessionOptions {
813 client
814 .unwrap_or_default()
815 .into_session_options(default_name)
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821 use std::{
822 ffi::OsStr,
823 sync::{Mutex, OnceLock},
824 };
825
826 fn env_lock() -> &'static Mutex<()> {
834 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
835 LOCK.get_or_init(|| Mutex::new(()))
836 }
837
838 struct EnvVarGuard {
839 key: &'static str,
840 previous: Option<std::ffi::OsString>,
841 }
842
843 impl EnvVarGuard {
844 fn set(key: &'static str, value: impl AsRef<OsStr>) -> Self {
845 let previous = env::var_os(key);
846 env::set_var(key, value);
847 Self { key, previous }
848 }
849
850 fn unset(key: &'static str) -> Self {
851 let previous = env::var_os(key);
852 env::remove_var(key);
853 Self { key, previous }
854 }
855 }
856
857 impl Drop for EnvVarGuard {
858 fn drop(&mut self) {
859 match &self.previous {
860 Some(previous) => env::set_var(self.key, previous),
861 None => env::remove_var(self.key),
862 }
863 }
864 }
865
866 const ALL_REST_LIMIT_VARS: &[&str] = &[
867 "CODEX_APP_SERVER_REST_MAX_SESSIONS",
868 "CODEX_APP_SERVER_REST_MAX_ONE_SHOT_CONCURRENCY",
869 "CODEX_APP_SERVER_REST_MAX_SESSION_CALL_CONCURRENCY",
870 "CODEX_APP_SERVER_REST_MAX_SESSION_CALL_CONCURRENCY_PER_SESSION",
871 "CODEX_APP_SERVER_REST_MAX_POLL_TIMEOUT_MS",
872 "CODEX_APP_SERVER_REST_MAX_TEXT_TURN_DURATION_MS",
873 "CODEX_APP_SERVER_REST_MAX_TEXT_TURN_OUTPUT_BYTES",
874 "CODEX_APP_SERVER_REST_PENDING_REQUEST_TTL_MS",
875 "CODEX_APP_SERVER_REST_MAX_PENDING_REQUESTS_PER_SESSION",
876 "CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY",
877 "CODEX_APP_SERVER_REST_IDLE_SESSION_TTL_MS",
878 "CODEX_APP_SERVER_REST_COMPATIBILITY_TTL_MS",
879 "CODEX_APP_SERVER_REST_SSE_KEEP_ALIVE_MS",
880 ];
881
882 fn clear_all_rest_limit_vars() -> Vec<EnvVarGuard> {
886 ALL_REST_LIMIT_VARS
887 .iter()
888 .map(|var| EnvVarGuard::unset(var))
889 .collect()
890 }
891
892 #[test]
893 fn try_from_env_uses_defaults_when_every_variable_is_absent() {
894 let _lock = env_lock()
895 .lock()
896 .unwrap_or_else(|poisoned| poisoned.into_inner());
897 let _cleared = clear_all_rest_limit_vars();
898
899 let limits = RestLimits::try_from_env().expect("all-absent env should use defaults");
900 let default = RestLimits::default();
901 assert_eq!(limits.max_sessions, default.max_sessions);
902 assert_eq!(
903 limits.max_session_call_concurrency_per_session,
904 default.max_session_call_concurrency_per_session
905 );
906 assert_eq!(limits.max_poll_timeout, default.max_poll_timeout);
907 assert_eq!(
908 limits.events_channel_capacity,
909 default.events_channel_capacity
910 );
911 assert_eq!(
912 limits.sse_keep_alive_interval,
913 default.sse_keep_alive_interval
914 );
915 }
916
917 #[test]
918 fn try_from_env_parses_valid_overrides() {
919 let _lock = env_lock()
920 .lock()
921 .unwrap_or_else(|poisoned| poisoned.into_inner());
922 let _cleared = clear_all_rest_limit_vars();
923 let _max_sessions = EnvVarGuard::set("CODEX_APP_SERVER_REST_MAX_SESSIONS", "42");
924 let _poll_timeout = EnvVarGuard::set("CODEX_APP_SERVER_REST_MAX_POLL_TIMEOUT_MS", "5000");
925 let _keep_alive = EnvVarGuard::set("CODEX_APP_SERVER_REST_SSE_KEEP_ALIVE_MS", "2500");
926
927 let limits = RestLimits::try_from_env().expect("well-formed overrides should parse");
928
929 assert_eq!(limits.max_sessions, 42);
930 assert_eq!(limits.max_poll_timeout, Duration::from_millis(5000));
931 assert_eq!(limits.sse_keep_alive_interval, Duration::from_millis(2500));
932 assert_eq!(
934 limits.max_one_shot_concurrency,
935 RestLimits::default().max_one_shot_concurrency
936 );
937 }
938
939 #[test]
940 fn try_from_env_reports_malformed_values_instead_of_defaulting() {
941 let _lock = env_lock()
942 .lock()
943 .unwrap_or_else(|poisoned| poisoned.into_inner());
944 let _cleared = clear_all_rest_limit_vars();
945 let _bad = EnvVarGuard::set("CODEX_APP_SERVER_REST_MAX_SESSIONS", "not-a-number");
946
947 let error = RestLimits::try_from_env()
948 .expect_err("a malformed override must not silently fall back to the default");
949
950 assert_eq!(error.var, "CODEX_APP_SERVER_REST_MAX_SESSIONS");
951 assert_eq!(error.value, "not-a-number");
952 }
953
954 #[test]
955 fn try_from_env_reports_empty_string_as_malformed() {
956 let _lock = env_lock()
957 .lock()
958 .unwrap_or_else(|poisoned| poisoned.into_inner());
959 let _cleared = clear_all_rest_limit_vars();
960 let _bad = EnvVarGuard::set("CODEX_APP_SERVER_REST_MAX_POLL_TIMEOUT_MS", "");
961
962 let error = RestLimits::try_from_env()
963 .expect_err("an empty override must not silently fall back to the default");
964
965 assert_eq!(error.var, "CODEX_APP_SERVER_REST_MAX_POLL_TIMEOUT_MS");
966 }
967
968 #[test]
969 fn try_from_env_reports_negative_values_as_malformed() {
970 let _lock = env_lock()
971 .lock()
972 .unwrap_or_else(|poisoned| poisoned.into_inner());
973 let _cleared = clear_all_rest_limit_vars();
974 let _bad = EnvVarGuard::set("CODEX_APP_SERVER_REST_MAX_SESSIONS", "-1");
975
976 let error = RestLimits::try_from_env()
977 .expect_err("a negative override must not silently fall back to the default");
978
979 assert_eq!(error.var, "CODEX_APP_SERVER_REST_MAX_SESSIONS");
980 }
981
982 #[test]
987 fn try_from_env_rejects_a_zero_events_channel_capacity_at_load_time() {
988 let _lock = env_lock()
989 .lock()
990 .unwrap_or_else(|poisoned| poisoned.into_inner());
991 let _cleared = clear_all_rest_limit_vars();
992 let _bad = EnvVarGuard::set("CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY", "0");
993
994 let error = RestLimits::try_from_env()
995 .expect_err("a zero events channel capacity is not constructible and must be rejected");
996
997 assert_eq!(error.var, "CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY");
998 assert!(
999 error.to_string().contains("positive"),
1000 "the error should say what was expected, got: {error}"
1001 );
1002 }
1003
1004 #[test]
1005 fn try_from_env_parses_a_valid_events_channel_capacity_override() {
1006 let _lock = env_lock()
1007 .lock()
1008 .unwrap_or_else(|poisoned| poisoned.into_inner());
1009 let _cleared = clear_all_rest_limit_vars();
1010 let _capacity = EnvVarGuard::set("CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY", "7");
1011
1012 let limits = RestLimits::try_from_env().expect("a well-formed override should parse");
1013
1014 assert_eq!(limits.events_channel_capacity, 7);
1015 }
1016
1017 #[test]
1018 fn try_from_env_reports_a_malformed_events_channel_capacity_instead_of_defaulting() {
1019 let _lock = env_lock()
1020 .lock()
1021 .unwrap_or_else(|poisoned| poisoned.into_inner());
1022 let _cleared = clear_all_rest_limit_vars();
1023 let _bad = EnvVarGuard::set(
1024 "CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY",
1025 "not-a-number",
1026 );
1027
1028 let error = RestLimits::try_from_env()
1029 .expect_err("a malformed override must not silently fall back to the default");
1030
1031 assert_eq!(error.var, "CODEX_APP_SERVER_REST_EVENTS_CHANNEL_CAPACITY");
1032 assert_eq!(error.value, "not-a-number");
1033 }
1034
1035 #[test]
1036 #[should_panic(expected = "CODEX_APP_SERVER_REST_MAX_SESSIONS")]
1037 fn from_env_panics_on_malformed_override() {
1038 let _lock = env_lock()
1039 .lock()
1040 .unwrap_or_else(|poisoned| poisoned.into_inner());
1041 let _cleared = clear_all_rest_limit_vars();
1042 let _bad = EnvVarGuard::set("CODEX_APP_SERVER_REST_MAX_SESSIONS", "nope");
1043
1044 let _ = RestLimits::from_env();
1045 }
1046}