diff --git a/.github/scripts/verify_cargo_workspace_manifests.py b/.github/scripts/verify_cargo_workspace_manifests.py new file mode 100644 index 000000000..ad146bc28 --- /dev/null +++ b/.github/scripts/verify_cargo_workspace_manifests.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +"""Verify that codex-rs crates inherit workspace metadata, lints, and names. + +This keeps `cargo clippy` aligned with the workspace lint policy by ensuring +each crate opts into `[lints] workspace = true`, and it also checks the crate +name conventions for top-level `codex-rs/*` crates and `codex-rs/utils/*` +crates. +""" + +from __future__ import annotations + +import sys +import tomllib +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +CARGO_RS_ROOT = ROOT / "codex-rs" +WORKSPACE_PACKAGE_FIELDS = ("version", "edition", "license") +TOP_LEVEL_NAME_EXCEPTIONS = { + "windows-sandbox-rs": "codex-windows-sandbox", +} +UTILITY_NAME_EXCEPTIONS = { + "path-utils": "codex-utils-path", +} + + +def main() -> int: + failures = [ + (path.relative_to(ROOT), errors) + for path in cargo_manifests() + if (errors := manifest_errors(path)) + ] + if not failures: + return 0 + + print( + "Cargo manifests under codex-rs must inherit workspace package metadata and " + "opt into workspace lints." + ) + print( + "Cargo only applies `codex-rs/Cargo.toml` `[workspace.lints.clippy]` " + "entries to a crate when that crate declares:" + ) + print() + print("[lints]") + print("workspace = true") + print() + print( + "Without that opt-in, `cargo clippy` can miss violations that Bazel clippy " + "catches." + ) + print() + print( + "Package-name checks apply to `codex-rs//Cargo.toml` and " + "`codex-rs/utils//Cargo.toml`." + ) + print() + for path, errors in failures: + print(f"{path}:") + for error in errors: + print(f" - {error}") + + return 1 + + +def manifest_errors(path: Path) -> list[str]: + manifest = load_manifest(path) + package = manifest.get("package") + if not isinstance(package, dict): + return [] + + errors = [] + for field in WORKSPACE_PACKAGE_FIELDS: + if not is_workspace_reference(package.get(field)): + errors.append(f"set `{field}.workspace = true` in `[package]`") + + lints = manifest.get("lints") + if not (isinstance(lints, dict) and lints.get("workspace") is True): + errors.append("add `[lints]` with `workspace = true`") + + expected_name = expected_package_name(path) + if expected_name is not None: + actual_name = package.get("name") + if actual_name != expected_name: + errors.append( + f"set `[package].name` to `{expected_name}` (found `{actual_name}`)" + ) + + return errors + + +def expected_package_name(path: Path) -> str | None: + parts = path.relative_to(CARGO_RS_ROOT).parts + if len(parts) == 2 and parts[1] == "Cargo.toml": + directory = parts[0] + return TOP_LEVEL_NAME_EXCEPTIONS.get( + directory, + directory if directory.startswith("codex-") else f"codex-{directory}", + ) + if len(parts) == 3 and parts[0] == "utils" and parts[2] == "Cargo.toml": + directory = parts[1] + return UTILITY_NAME_EXCEPTIONS.get(directory, f"codex-utils-{directory}") + return None + + +def is_workspace_reference(value: object) -> bool: + return isinstance(value, dict) and value.get("workspace") is True + + +def load_manifest(path: Path) -> dict: + return tomllib.loads(path.read_text()) + + +def cargo_manifests() -> list[Path]: + return sorted( + path + for path in CARGO_RS_ROOT.rglob("Cargo.toml") + if path != CARGO_RS_ROOT / "Cargo.toml" + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c5390040..d32a8fd0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,9 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Verify codex-rs Cargo manifests inherit workspace settings + run: python3 .github/scripts/verify_cargo_workspace_manifests.py + - name: Setup pnpm uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 with: diff --git a/codex-rs/ansi-escape/Cargo.toml b/codex-rs/ansi-escape/Cargo.toml index a10dbf913..3ebad2bde 100644 --- a/codex-rs/ansi-escape/Cargo.toml +++ b/codex-rs/ansi-escape/Cargo.toml @@ -8,6 +8,9 @@ license.workspace = true name = "codex_ansi_escape" path = "src/lib.rs" +[lints] +workspace = true + [dependencies] ansi-to-tui = { workspace = true } ratatui = { workspace = true, features = [ diff --git a/codex-rs/app-server/tests/common/Cargo.toml b/codex-rs/app-server/tests/common/Cargo.toml index 4eef03e96..c71e5797a 100644 --- a/codex-rs/app-server/tests/common/Cargo.toml +++ b/codex-rs/app-server/tests/common/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true [lib] path = "lib.rs" +[lints] +workspace = true + [dependencies] anyhow = { workspace = true } base64 = { workspace = true } diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 427f6cc1f..8072ff45f 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -27,7 +27,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { }, supported_in_api: preset.supported_in_api, priority, - upgrade: preset.upgrade.as_ref().map(|u| u.into()), + upgrade: preset.upgrade.as_ref().map(Into::into), base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, diff --git a/codex-rs/backend-client/Cargo.toml b/codex-rs/backend-client/Cargo.toml index 8279dba63..96e9d7359 100644 --- a/codex-rs/backend-client/Cargo.toml +++ b/codex-rs/backend-client/Cargo.toml @@ -8,6 +8,9 @@ publish = false [lib] path = "src/lib.rs" +[lints] +workspace = true + [dependencies] anyhow = "1" serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/codex-backend-openapi-models/Cargo.toml b/codex-rs/codex-backend-openapi-models/Cargo.toml index f9bad4a49..ed3a1043d 100644 --- a/codex-rs/codex-backend-openapi-models/Cargo.toml +++ b/codex-rs/codex-backend-openapi-models/Cargo.toml @@ -8,6 +8,9 @@ license.workspace = true name = "codex_backend_openapi_models" path = "src/lib.rs" +[lints] +workspace = true + # Important: generated code often violates our workspace lints. # Allow unwrap/expect in this crate so the workspace builds cleanly # after models are regenerated. diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index 1e0b8d6cc..0fcf81281 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true [lib] path = "lib.rs" +[lints] +workspace = true + [dependencies] anyhow = { workspace = true } assert_cmd = { workspace = true } diff --git a/codex-rs/debug-client/Cargo.toml b/codex-rs/debug-client/Cargo.toml index b220ebd36..14dcb5600 100644 --- a/codex-rs/debug-client/Cargo.toml +++ b/codex-rs/debug-client/Cargo.toml @@ -4,6 +4,9 @@ version.workspace = true edition.workspace = true license.workspace = true +[lints] +workspace = true + [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index 762c11cb6..2ada10e37 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -1,3 +1,4 @@ +#![allow(clippy::expect_used)] use std::io::BufRead; use std::io::BufReader; use std::io::Write; diff --git a/codex-rs/debug-client/src/output.rs b/codex-rs/debug-client/src/output.rs index ec71cf2b2..ca3ac9d9c 100644 --- a/codex-rs/debug-client/src/output.rs +++ b/codex-rs/debug-client/src/output.rs @@ -1,3 +1,4 @@ +#![allow(clippy::expect_used)] use std::io; use std::io::IsTerminal; use std::io::Write; diff --git a/codex-rs/debug-client/src/reader.rs b/codex-rs/debug-client/src/reader.rs index 48841f699..ed401bb10 100644 --- a/codex-rs/debug-client/src/reader.rs +++ b/codex-rs/debug-client/src/reader.rs @@ -1,3 +1,4 @@ +#![allow(clippy::expect_used)] use std::io::BufRead; use std::io::BufReader; use std::process::ChildStdout; @@ -113,7 +114,7 @@ fn handle_server_request( stdin: &Arc>>, output: &Output, ) -> anyhow::Result<()> { - let server_request = match ServerRequest::try_from(request.clone()) { + let server_request = match ServerRequest::try_from(request) { Ok(server_request) => server_request, Err(_) => return Ok(()), }; diff --git a/codex-rs/feedback/Cargo.toml b/codex-rs/feedback/Cargo.toml index 73803af86..dd8795c8c 100644 --- a/codex-rs/feedback/Cargo.toml +++ b/codex-rs/feedback/Cargo.toml @@ -4,6 +4,9 @@ version.workspace = true edition.workspace = true license.workspace = true +[lints] +workspace = true + [dependencies] anyhow = { workspace = true } codex-protocol = { workspace = true } diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index 19c6abffd..9b2cf33d8 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -95,10 +95,12 @@ impl CodexFeedback { pub fn snapshot(&self, session_id: Option) -> FeedbackSnapshot { let bytes = { + #[allow(clippy::expect_used)] let guard = self.inner.ring.lock().expect("mutex poisoned"); guard.snapshot_bytes() }; let tags = { + #[allow(clippy::expect_used)] let guard = self.inner.tags.lock().expect("mutex poisoned"); guard.clone() }; @@ -324,7 +326,7 @@ impl FeedbackSnapshot { use sentry::protocol::Values; event.exception = Values::from(vec![Exception { - ty: title.clone(), + ty: title, value: Some(r.to_string()), ..Default::default() }]); @@ -430,6 +432,7 @@ where return; } + #[allow(clippy::expect_used)] let mut guard = self.inner.tags.lock().expect("mutex poisoned"); for (key, value) in visitor.tags { if guard.len() >= MAX_FEEDBACK_TAGS && !guard.contains_key(&key) { diff --git a/codex-rs/file-search/Cargo.toml b/codex-rs/file-search/Cargo.toml index 3802ed5fe..7a62a4a1d 100644 --- a/codex-rs/file-search/Cargo.toml +++ b/codex-rs/file-search/Cargo.toml @@ -12,6 +12,9 @@ path = "src/main.rs" name = "codex_file_search" path = "src/lib.rs" +[lints] +workspace = true + [dependencies] anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index 83e5d9880..95cfa8be4 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -195,10 +195,10 @@ pub fn create_session( threads: threads.get(), compute_indices, respect_gitignore, - cancelled: cancelled.clone(), + cancelled, shutdown: Arc::new(AtomicBool::new(false)), reporter, - work_tx: work_tx.clone(), + work_tx, }); let matcher_inner = inner.clone(); @@ -611,13 +611,14 @@ struct RunReporter { impl SessionReporter for RunReporter { fn on_update(&self, snapshot: &FileSearchSnapshot) { - #[expect(clippy::unwrap_used)] + #[allow(clippy::unwrap_used)] let mut guard = self.snapshot.write().unwrap(); *guard = snapshot.clone(); } fn on_complete(&self) { let (cv, mutex) = &self.completed; + #[allow(clippy::unwrap_used)] let mut completed = mutex.lock().unwrap(); *completed = true; cv.notify_all(); @@ -627,10 +628,15 @@ impl SessionReporter for RunReporter { impl RunReporter { fn wait_for_complete(&self) -> FileSearchSnapshot { let (cv, mutex) = &self.completed; + #[allow(clippy::unwrap_used)] let mut completed = mutex.lock().unwrap(); while !*completed { - completed = cv.wait(completed).unwrap(); + #[allow(clippy::unwrap_used)] + { + completed = cv.wait(completed).unwrap(); + } } + #[allow(clippy::unwrap_used)] self.snapshot.read().unwrap().clone() } } diff --git a/codex-rs/file-search/src/main.rs b/codex-rs/file-search/src/main.rs index 4715d1bd6..beb4b18a6 100644 --- a/codex-rs/file-search/src/main.rs +++ b/codex-rs/file-search/src/main.rs @@ -27,8 +27,11 @@ struct StdioReporter { impl Reporter for StdioReporter { fn report_match(&self, file_match: &FileMatch) { if self.write_output_as_json { - println!("{}", serde_json::to_string(&file_match).unwrap()); + #[allow(clippy::unwrap_used)] + let json = serde_json::to_string(file_match).unwrap(); + println!("{json}"); } else if self.show_indices { + #[allow(clippy::expect_used)] let indices = file_match .indices .as_ref() @@ -61,7 +64,9 @@ impl Reporter for StdioReporter { fn warn_matches_truncated(&self, total_match_count: usize, shown_match_count: usize) { if self.write_output_as_json { let value = json!({"matches_truncated": true}); - println!("{}", serde_json::to_string(&value).unwrap()); + #[allow(clippy::unwrap_used)] + let json = serde_json::to_string(&value).unwrap(); + println!("{json}"); } else { eprintln!( "Warning: showing {shown_match_count} out of {total_match_count} results. Provide a more specific pattern or increase the --limit.", diff --git a/codex-rs/mcp-server/tests/common/Cargo.toml b/codex-rs/mcp-server/tests/common/Cargo.toml index 83f2c5369..d19f67363 100644 --- a/codex-rs/mcp-server/tests/common/Cargo.toml +++ b/codex-rs/mcp-server/tests/common/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true [lib] path = "lib.rs" +[lints] +workspace = true + [dependencies] anyhow = { workspace = true } codex-core = { workspace = true } diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index 6ef6c690a..1d9a3375e 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "codex-network-proxy" -edition = "2024" -version = { workspace = true } +edition.workspace = true +version.workspace = true license.workspace = true [lib] diff --git a/codex-rs/shell-escalation/Cargo.toml b/codex-rs/shell-escalation/Cargo.toml index fbc5bcd8c..1f6ded3e4 100644 --- a/codex-rs/shell-escalation/Cargo.toml +++ b/codex-rs/shell-escalation/Cargo.toml @@ -8,6 +8,9 @@ version.workspace = true name = "codex-execve-wrapper" path = "src/bin/main_execve_wrapper.rs" +[lints] +workspace = true + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index 4e7f09b9a..34b856216 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -601,7 +601,7 @@ mod tests { let execve_wrapper_str = execve_wrapper.to_string_lossy().to_string(); let server = EscalateServer::new( PathBuf::from("/bin/zsh"), - execve_wrapper.clone(), + execve_wrapper, DeterministicEscalationPolicy { decision: EscalationDecision::run(), }, diff --git a/codex-rs/shell-escalation/src/unix/socket.rs b/codex-rs/shell-escalation/src/unix/socket.rs index cfff1268a..a364608fc 100644 --- a/codex-rs/shell-escalation/src/unix/socket.rs +++ b/codex-rs/shell-escalation/src/unix/socket.rs @@ -62,7 +62,7 @@ fn extract_fds(control: &[u8]) -> Vec { let fd_count: usize = { // `cmsghdr::cmsg_len` is not typed consistently across targets, so normalize it // before doing the size arithmetic. - #[allow(clippy::useless_conversion)] + #[allow(clippy::useless_conversion, clippy::expect_used)] let cmsg_data_len = usize::try_from(unsafe { (*cmsg).cmsg_len }) .expect("cmsghdr length fits") - unsafe { libc::CMSG_LEN(0) as usize }; diff --git a/codex-rs/utils/pty/Cargo.toml b/codex-rs/utils/pty/Cargo.toml index cf98ac490..7196cf531 100644 --- a/codex-rs/utils/pty/Cargo.toml +++ b/codex-rs/utils/pty/Cargo.toml @@ -1,5 +1,5 @@ [package] -edition = "2024" +edition.workspace = true license.workspace = true name = "codex-utils-pty" version.workspace = true diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index 0fb219257..6d49f81a3 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] build = "build.rs" -edition = "2024" +edition.workspace = true license.workspace = true name = "codex-windows-sandbox" version.workspace = true @@ -17,6 +17,9 @@ path = "src/bin/setup_main.rs" name = "codex-command-runner" path = "src/bin/command_runner.rs" +[lints] +workspace = true + [dependencies] anyhow = "1.0" base64 = { workspace = true }