Files
codex/codex-rs/utils/pty
Michael Bolin 9a8730f31e ci: verify codex-rs Cargo manifests inherit workspace settings (#16353)
## Why

Bazel clippy now catches lints that `cargo clippy` can still miss when a
crate under `codex-rs` forgets to opt into workspace lints. The concrete
example here was `codex-rs/app-server/tests/common/Cargo.toml`: Bazel
flagged a clippy violation in `models_cache.rs`, but Cargo did not
because that crate inherited workspace package metadata without
declaring `[lints] workspace = true`.

We already mirror the workspace clippy deny list into Bazel after
[#15955](https://github.com/openai/codex/pull/15955), so we also need a
repo-side check that keeps every `codex-rs` manifest opted into the same
workspace settings.

## What changed

- add `.github/scripts/verify_cargo_workspace_manifests.py`, which
parses every `codex-rs/**/Cargo.toml` with `tomllib` and verifies:
  - `version.workspace = true`
  - `edition.workspace = true`
  - `license.workspace = true`
  - `[lints] workspace = true`
- top-level crate names follow the `codex-*` / `codex-utils-*`
conventions, with explicit exceptions for `windows-sandbox-rs` and
`utils/path-utils`
- run that script in `.github/workflows/ci.yml`
- update the current outlier manifests so the check is enforceable
immediately
- fix the newly exposed clippy violations in the affected crates
(`app-server/tests/common`, `file-search`, `feedback`,
`shell-escalation`, and `debug-client`)






---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16353).
* #16351
* __->__ #16353
2026-03-31 21:59:28 +00:00
..

codex-utils-pty

Lightweight helpers for spawning interactive processes either under a PTY (pseudo terminal) or regular pipes. The public API is minimal and mirrors both backends so callers can switch based on their needs (e.g., enabling or disabling TTY).

API surface

  • spawn_pty_process(program, args, cwd, env, arg0, size)SpawnedProcess
  • spawn_pipe_process(program, args, cwd, env, arg0)SpawnedProcess
  • spawn_pipe_process_no_stdin(program, args, cwd, env, arg0)SpawnedProcess
  • combine_output_receivers(stdout_rx, stderr_rx)broadcast::Receiver<Vec<u8>>
  • conpty_supported()bool (Windows only; always true elsewhere)
  • TerminalSize { rows, cols } selects PTY dimensions in character cells.
  • ProcessHandle exposes:
    • writer_sender()mpsc::Sender<Vec<u8>> (stdin)
    • resize(TerminalSize)
    • close_stdin()
    • has_exited(), exit_code(), terminate()
  • SpawnedProcess bundles session, stdout_rx, stderr_rx, and exit_rx (oneshot exit code).

Usage examples

use std::collections::HashMap;
use std::path::Path;
use codex_utils_pty::combine_output_receivers;
use codex_utils_pty::spawn_pty_process;
use codex_utils_pty::TerminalSize;

# tokio_test::block_on(async {
let env_map: HashMap<String, String> = std::env::vars().collect();
let spawned = spawn_pty_process(
    "bash",
    &["-lc".into(), "echo hello".into()],
    Path::new("."),
    &env_map,
    &None,
    TerminalSize::default(),
).await?;

let writer = spawned.session.writer_sender();
writer.send(b"exit\n".to_vec()).await?;

// Collect output until the process exits.
let mut output_rx = combine_output_receivers(spawned.stdout_rx, spawned.stderr_rx);
let mut collected = Vec::new();
while let Ok(chunk) = output_rx.try_recv() {
    collected.extend_from_slice(&chunk);
}
let exit_code = spawned.exit_rx.await.unwrap_or(-1);
# let _ = (collected, exit_code);
# anyhow::Ok(())
# });

Swap in spawn_pipe_process for a non-TTY subprocess; the rest of the API stays the same. Use spawn_pipe_process_no_stdin to force stdin closed (commands that read stdin will see EOF immediately).

Tests

Unit tests live in src/lib.rs and cover both backends (PTY Python REPL and pipe-based stdin roundtrip). Run with:

cargo test -p codex-utils-pty -- --nocapture