diff --git a/.github/scripts/verify_cargo_workspace_manifests.py b/.github/scripts/verify_cargo_workspace_manifests.py new file mode 100644 index 0000000000..ad146bc286 --- /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 6c53900401..d32a8fd0a1 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 a10dbf9134..3ebad2bdef 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 4eef03e969..c71e5797ad 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 427f6cc1f2..8072ff45f6 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 8279dba630..96e9d73599 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 f9bad4a494..ed3a1043d6 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 1e0b8d6cc2..0fcf812815 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 b220ebd367..14dcb5600a 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 762c11cb63..2ada10e377 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 ec71cf2b24..ca3ac9d9cb 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 48841f699d..ed401bb100 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 73803af86a..dd8795c8ca 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 19c6abffdf..9b2cf33d8d 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 3802ed5fe3..7a62a4a1df 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 83e5d98803..95cfa8be40 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 4715d1bd62..beb4b18a64 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 83f2c53697..d19f673634 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 6ef6c690ad..5313690cdb 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codex-network-proxy" -edition = "2024" +edition.workspace = true version = { workspace = true } license.workspace = true diff --git a/codex-rs/shell-escalation/Cargo.toml b/codex-rs/shell-escalation/Cargo.toml index fbc5bcd8c7..1f6ded3e40 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 4e7f09b9ad..34b8562160 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 cfff1268a3..44eb6a7473 100644 --- a/codex-rs/shell-escalation/src/unix/socket.rs +++ b/codex-rs/shell-escalation/src/unix/socket.rs @@ -59,15 +59,18 @@ fn extract_fds(control: &[u8]) -> Vec { let ty = unsafe { (*cmsg).cmsg_type }; if level == libc::SOL_SOCKET && ty == libc::SCM_RIGHTS { let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::() }; - let fd_count: usize = { + let Some(cmsg_data_len) = ({ // `cmsghdr::cmsg_len` is not typed consistently across targets, so normalize it // before doing the size arithmetic. #[allow(clippy::useless_conversion)] - let cmsg_data_len = usize::try_from(unsafe { (*cmsg).cmsg_len }) - .expect("cmsghdr length fits") - - unsafe { libc::CMSG_LEN(0) as usize }; - cmsg_data_len / size_of::() + usize::try_from(unsafe { (*cmsg).cmsg_len }) + .ok() + .and_then(|len| len.checked_sub(unsafe { libc::CMSG_LEN(0) as usize })) + }) else { + cmsg = unsafe { libc::CMSG_NXTHDR(&hdr, cmsg) }; + continue; }; + let fd_count = cmsg_data_len / size_of::(); for i in 0..fd_count { let fd = unsafe { data_ptr.add(i).read() }; fds.push(unsafe { OwnedFd::from_raw_fd(fd) }); diff --git a/codex-rs/utils/pty/Cargo.toml b/codex-rs/utils/pty/Cargo.toml index cf98ac4904..7196cf5312 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 0fb2192578..6d49f81a31 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 }