Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 49 additions & 9 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,17 +527,27 @@ pub async fn run_main_with_transport_options(

let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
config_warnings.push(message);
(
config_manager.load_default_config().await.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?,
false,
)
let mut config = config_manager.load_default_config().await.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading default config after config error: {e}"),
)
})?;
config
.features
.disable(codex_features::Feature::Sqlite)
.map_err(std::io::Error::other)?;
(config, false)
}
};
if !config.features.enabled(codex_features::Feature::Sqlite) {
config_warnings.push(ConfigWarningNotification {
summary: "SQLite is disabled. Codex is running in degraded mode; SQLite-dependent features and operations are unavailable.".to_string(),
details: None,
path: None,
range: None,
});
}

let otel = codex_core::otel_init::build_provider(
&config,
Expand Down Expand Up @@ -1208,6 +1218,13 @@ struct StateDbInitResult {
async fn init_sqlite_state_db_with_fresh_start_on_corruption(
config: &Config,
) -> anyhow::Result<StateDbInitResult> {
if !config.features.enabled(codex_features::Feature::Sqlite) {
return Ok(StateDbInitResult {
state_db: None,
recovery_notice: None,
});
}

let mut attempted_backups = HashSet::new();
let mut recovered_databases = Vec::new();
loop {
Expand Down Expand Up @@ -1368,9 +1385,12 @@ mod tests {
use super::loader_overrides_with_test_user_config_file;
#[cfg(debug_assertions)]
use codex_config::LoaderOverrides;
use codex_core::config::ConfigBuilder;
use codex_features::Feature;
#[cfg(debug_assertions)]
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;

#[test]
fn log_format_from_env_value_matches_json_values_case_insensitively() {
Expand All @@ -1390,6 +1410,26 @@ mod tests {
assert_eq!(LogFormat::from_env_value(Some("jsonl")), LogFormat::Default);
}

#[tokio::test]
async fn sqlite_state_db_can_be_explicitly_disabled() -> anyhow::Result<()> {
let temp_dir = TempDir::new()?;
let mut config = ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.build()
.await?;
config.features.disable(Feature::Sqlite)?;
let occupied_sqlite_home = temp_dir.path().join("sqlite-home");
std::fs::write(&occupied_sqlite_home, "occupied")?;
config.sqlite_home = occupied_sqlite_home.clone();

let result = super::init_sqlite_state_db_with_fresh_start_on_corruption(&config).await?;

assert!(result.state_db.is_none());
assert!(result.recovery_notice.is_none());
assert_eq!(std::fs::read_to_string(occupied_sqlite_home)?, "occupied");
Ok(())
}

#[cfg(debug_assertions)]
#[test]
fn debug_test_user_config_file_overrides_loader_path() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,9 @@ impl ExternalAgentConfigRequestProcessor {
pub(crate) async fn read_import_histories(
&self,
) -> Result<ExternalAgentConfigImportHistoriesReadResponse, JSONRPCErrorError> {
let state_db = self
.state_db
.as_ref()
.ok_or_else(|| internal_error("state database is unavailable"))?;
let Some(state_db) = self.state_db.as_ref() else {
return Ok(ExternalAgentConfigImportHistoriesReadResponse { data: Vec::new() });
};
let histories = state_db
.external_agent_config_import_history_records()
.await
Expand Down
17 changes: 17 additions & 0 deletions codex-rs/app-server/src/request_processors/thread_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,12 @@ impl ThreadRequestProcessor {
}

async fn memory_reset_response_inner(&self) -> Result<MemoryResetResponse, JSONRPCErrorError> {
if !self.config.features.enabled(Feature::Sqlite) {
return Err(invalid_request(
"memory reset requires SQLite, which is disabled",
));
}

let state_db = self
.state_db
.clone()
Expand Down Expand Up @@ -1615,6 +1621,12 @@ impl ThreadRequestProcessor {
&self,
params: ThreadMetadataUpdateParams,
) -> Result<ThreadMetadataUpdateResponse, JSONRPCErrorError> {
if !self.config.features.enabled(Feature::Sqlite) {
return Err(invalid_request(
"thread metadata updates require SQLite, which is disabled",
));
}

let ThreadMetadataUpdateParams {
thread_id,
git_info,
Expand Down Expand Up @@ -1960,6 +1972,11 @@ impl ThreadRequestProcessor {
)),
(None, None) => None,
};
if relation_filter.is_some() && !self.config.features.enabled(Feature::Sqlite) {
return Err(invalid_request(
"thread relationship filters require SQLite, which is disabled",
));
}

let requested_page_size = limit
.map(|value| value as usize)
Expand Down
35 changes: 35 additions & 0 deletions codex-rs/app-server/tests/suite/strict_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,38 @@ foo = "bar"

Ok(())
}

#[test]
fn non_strict_config_fallback_disables_sqlite() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
model = 123
[features]
sqlite = false
"#,
)?;

let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex-app-server")?)
.env("CODEX_HOME", codex_home.path())
.env(codex_state::SQLITE_HOME_ENV, codex_home.path())
.env(
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
codex_home.path().join("managed_config.toml"),
)
.args(["--listen", "off"])
.output()?;

assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr)?;
assert!(
stderr.contains("no transport configured"),
"expected startup to reach transport validation, got: {stderr}"
);
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}
34 changes: 34 additions & 0 deletions codex-rs/app-server/tests/suite/v2/config_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use tokio::time::timeout;
// Bazel CI can spend tens of seconds starting app-server subprocesses or
// processing config RPCs under load.
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
const SQLITE_DISABLED_WARNING_SUMMARY: &str = "SQLite is disabled. Codex is running in degraded mode; SQLite-dependent features and operations are unavailable.";

fn write_config(codex_home: &TempDir, contents: &str) -> Result<()> {
Ok(std::fs::write(
Expand All @@ -48,6 +49,39 @@ fn write_config(codex_home: &TempDir, contents: &str) -> Result<()> {
)?)
}

#[tokio::test]
async fn sqlite_disabled_emits_degraded_mode_warning() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(&codex_home, "[features]\nsqlite = false\n")?;
let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("CODEX_SQLITE_HOME", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let warning = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_matching_notification("SQLite disabled config warning", |message| {
message.method == "configWarning"
&& message
.params
.as_ref()
.and_then(|params| params.get("summary"))
.and_then(serde_json::Value::as_str)
== Some(SQLITE_DISABLED_WARNING_SUMMARY)
}),
)
.await??;

assert_eq!(
warning
.params
.as_ref()
.and_then(|params| params.get("summary"))
.and_then(serde_json::Value::as_str),
Some(SQLITE_DISABLED_WARNING_SUMMARY)
);
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_includes_allow_remote_control() -> Result<()> {
let codex_home = TempDir::new()?;
Expand Down
31 changes: 31 additions & 0 deletions codex-rs/app-server/tests/suite/v2/external_agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,37 @@ fn assert_import_response(response: ExternalAgentConfigImportResponse) -> String
response.import_id
}

#[tokio::test]
async fn external_agent_config_import_histories_are_empty_when_sqlite_is_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"[features]\nsqlite = false\n",
)?;
let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("CODEX_SQLITE_HOME", None)]).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

let request_id = mcp
.send_raw_request(
"externalAgentConfig/import/readHistories",
/*params*/ None,
)
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: ExternalAgentConfigImportHistoriesReadResponse = to_response(response)?;

assert!(response.data.is_empty());
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}

#[tokio::test]
async fn external_agent_config_import_sends_completion_notification_for_sync_only_import()
-> Result<()> {
Expand Down
41 changes: 37 additions & 4 deletions codex-rs/app-server/tests/suite/v2/memory_reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
#[tokio::test]
async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path())?;
create_config_toml(codex_home.path(), /*sqlite_enabled*/ true)?;
let state_db = init_state_db(codex_home.path()).await?;

let memory_root = codex_home.path().join("memories");
Expand Down Expand Up @@ -68,6 +68,37 @@ async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result
Ok(())
}

#[tokio::test]
async fn memory_reset_reports_sqlite_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), /*sqlite_enabled*/ false)?;
let memory_root = codex_home.path().join("memories");
tokio::fs::create_dir_all(&memory_root).await?;
tokio::fs::write(memory_root.join("MEMORY.md"), "stale memory\n").await?;

let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_raw_request("memory/reset", /*params*/ None)
.await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;

assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"memory reset requires SQLite, which is disabled"
);
assert!(memory_root.join("MEMORY.md").is_file());
for db in codex_state::runtime_db_paths(codex_home.path()) {
assert!(!db.path.exists(), "{} should not exist", db.label);
}
Ok(())
}

async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) -> Result<ThreadId> {
let now = Utc::now();
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?;
Expand Down Expand Up @@ -126,19 +157,20 @@ async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>> {
Ok(state_db)
}

fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
fn create_config_toml(codex_home: &Path, sqlite_enabled: bool) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
r#"
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "mock_provider"
suppress_unstable_features_warning = true

[features]
sqlite = true
sqlite = {sqlite_enabled}

[model_providers.mock_provider]
name = "Mock provider for test"
Expand All @@ -147,5 +179,6 @@ wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
"#,
),
)
}
38 changes: 38 additions & 0 deletions codex-rs/app-server/tests/suite/v2/thread_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,44 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
Ok(())
}

#[tokio::test]
async fn thread_list_relation_filters_report_sqlite_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"[features]\nsqlite = false\n",
)?;
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
cursor: None,
sort_key: None,
sort_direction: None,
model_providers: None,
source_kinds: None,
archived: None,
cwd: None,
use_state_db_only: false,
search_term: None,
parent_thread_id: Some(ThreadId::new().to_string()),
ancestor_thread_id: None,
limit: Some(10),
})
.await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;

assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"thread relationship filters require SQLite, which is disabled"
);
Ok(())
}

#[tokio::test]
async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()> {
let codex_home = TempDir::new()?;
Expand Down
Loading
Loading