Move the TUI slash command into the chatwidget module and rename the login helper module to match API provisioning behavior.
Co-authored-by: Codex <noreply@openai.com>
Move the reusable authorization-code callback server into codex-login::server, switch api-provision over to the shared PKCE/state/callback flow, and keep the TUI browser path alive even when auto-open fails.
Validation:
- cargo check -p codex-login --lib
- cargo fmt --all --manifest-path /home/dev-user/code/codex/codex-rs/Cargo.toml
- git diff --check
Co-authored-by: Codex <noreply@openai.com>
Skip redundant cargo-home cache saves in Windows test jobs to avoid post-test timeouts, and add the required argument comments in the login OAuth helper.
Co-authored-by: Codex <noreply@openai.com>
Skip /api-provision when the current Codex process already inherited
OPENAI_API_KEY, and otherwise persist the provisioned key to .env under
OPENAI_API_KEY instead of CODEX_API_KEY.
Validation:
- cargo test -p codex-login
- cargo test -p codex-tui
- just fix -p codex-login
- just fix -p codex-tui
- just fmt
Co-authored-by: Codex <noreply@openai.com>
Extract the browser-based provisioning flow from codex-login so the plain TUI can
reuse it. Add /api-provision to the CLI, persist CODEX_API_KEY to .env, and
hot-apply the key via ephemeral auth without touching auth.json.
Validation:
- cargo test -p codex-login
- cargo test -p codex-tui
- just fix -p codex-login
- just fix -p codex-tui
- just fmt
Co-authored-by: Codex <noreply@openai.com>
## Why
`SandboxCommand.program` represents an executable path, but keeping it
as `String` forced path-backed callers to run `to_string_lossy()` before
the sandbox layer ever touched the command. That loses fidelity earlier
than necessary and adds avoidable conversions in runtimes that already
have a `PathBuf`.
## What changed
- Changed `SandboxCommand.program` to `OsString`.
- Updated `SandboxManager::transform` to keep the program and argv in
`OsString` form until the `SandboxExecRequest` conversion boundary.
- Switched the path-backed `apply_patch` and `js_repl` runtimes to pass
`into_os_string()` instead of `to_string_lossy()`.
- Updated the remaining string-backed builders and tests to match the
new type while preserving the existing Linux helper `arg0` behavior.
## Verification
- `cargo test -p codex-sandboxing`
- `just argument-comment-lint -p codex-core -p codex-sandboxing`
- `cargo test -p codex-core` currently fails in unrelated existing
config tests: `config::tests::approvals_reviewer_*` and
`config::tests::smart_approvals_alias_*`
## Why
`token_data` is owned by `codex-login`, but `codex-core` was still
re-exporting it. That let callers pull auth token types through
`codex-core`, which keeps otherwise unrelated crates coupled to
`codex-core` and makes `codex-core` more of a build-graph bottleneck.
## What changed
- remove the `codex-core` re-export of `codex_login::token_data`
- update the remaining `codex-core` internals that used
`crate::token_data` to import `codex_login::token_data` directly
- update downstream callers in `codex-rs/chatgpt`,
`codex-rs/tui_app_server`, `codex-rs/app-server/tests/common`, and
`codex-rs/core/tests` to import `codex_login::token_data` directly
- add explicit `codex-login` workspace dependencies and refresh lock
metadata for crates that now depend on it directly
## Validation
- `cargo test -p codex-chatgpt --locked`
- `just argument-comment-lint`
- `just bazel-lock-update`
- `just bazel-lock-check`
## Notes
- attempted `cargo test -p codex-core --locked` and `cargo test -p
codex-core auth_refresh --locked`, but both ran out of disk while
linking `codex-core` test binaries in the local environment
## Problem
Codex already treated an existing top-level project `./.codex` directory
as protected, but there was a gap on first creation.
If `./.codex` did not exist yet, a turn could create files under it,
such as `./.codex/config.toml`, without going through the same approval
path as later modifications. That meant the initial write could bypass
the intended protection for project-local Codex state.
## What this changes
This PR closes that first-creation gap in the Unix enforcement layers:
- `codex-protocol`
- treat the top-level project `./.codex` path as a protected carveout
even when it does not exist yet
- avoid injecting the default carveout when the user already has an
explicit rule for that exact path
- macOS Seatbelt
- deny writes to both the exact protected path and anything beneath it,
so creating `./.codex` itself is blocked in addition to writes inside it
- Linux bubblewrap
- preserve the same protected-path behavior for first-time creation
under `./.codex`
- tests
- add protocol regressions for missing `./.codex` and explicit-rule
collisions
- add Unix sandbox coverage for blocking first-time `./.codex` creation
- tighten Seatbelt policy assertions around excluded subpaths
## Scope
This change is intentionally scoped to protecting the top-level project
`.codex` subtree from agent writes.
It does not make `.codex` unreadable, and it does not change the product
behavior around loading project skills from `.codex` when project config
is untrusted.
## Why this shape
The fix is pointed rather than broad:
- it preserves the current model of “project `.codex` is protected from
writes”
- it closes the security-relevant first-write hole
- it avoids folding a larger permissions-model redesign into this PR
## Validation
- `cargo test -p codex-protocol`
- `cargo test -p codex-sandboxing seatbelt`
- `cargo test -p codex-exec --test all
sandbox_blocks_first_time_dot_codex_creation -- --nocapture`
---------
Co-authored-by: Michael Bolin <mbolin@openai.com>
## Why
Skill metadata accepted a `permissions` block and stored the result on
`SkillMetadata`, but that data was never consumed by runtime behavior.
Leaving the dead parsing path in place makes it look like skills can
widen or otherwise influence execution permissions when, in practice,
declared skill permissions are ignored.
This change removes that misleading surface area so the skill metadata
model matches what the system actually uses.
## What changed
- removed `permission_profile` and `managed_network_override` from
`core-skills::SkillMetadata`
- stopped parsing `permissions` from skill metadata in
`core-skills/src/loader.rs`
- deleted the loader tests that only exercised the removed permissions
parsing path
- cleaned up dependent `SkillMetadata` constructors in tests and TUI
code that were only carrying `None` for those fields
## Testing
- `cargo test -p codex-core-skills`
- `cargo test -p codex-tui
submission_prefers_selected_duplicate_skill_path`
- `just argument-comment-lint`
## Summary
- resolve system bwrap from PATH instead of hardcoding /usr/bin/bwrap
- skip PATH entries that resolve inside the current workspace before
launching the sandbox helper
- keep the vendored bubblewrap fallback when no trusted system bwrap is
found
## Validation
- cargo test -p codex-core bwrap --lib
- cargo test -p codex-linux-sandbox
- just fix -p codex-core
- just fix -p codex-linux-sandbox
- just fmt
- just argument-comment-lint
- cargo clean
- [x] Polish tool suggest prompts to distinguish between missing
connectors and discoverable plugins, and be very precise about the
triggering conditions.
## TR;DR
Replicates the `/title` command from `tui` to `tui_app_server`.
## Problem
The classic `tui` crate supports customizing the terminal window/tab
title via `/title`, but the `tui_app_server` crate does not. Users on
the app-server path have no way to configure what their terminal title
shows (project name, status, spinner, thread, etc.), making it harder to
identify Codex sessions across tabs or windows.
## Mental model
The terminal title is a *status surface* -- conceptually parallel to the
footer status line. Both surfaces are configurable lists of items, both
share expensive inputs (git branch lookup, project root discovery), and
both must be refreshed at the same lifecycle points. This change ports
the classic `tui`'s design verbatim:
1. **`terminal_title.rs`** owns the low-level OSC write path and input
sanitization. It strips control characters and bidi/invisible codepoints
before placing untrusted text (model output, thread names, project
paths) inside an escape sequence.
2. **`title_setup.rs`** defines `TerminalTitleItem` (the 8 configurable
items) and `TerminalTitleSetupView` (the interactive picker that wraps
`MultiSelectPicker`).
3. **`status_surfaces.rs`** is the shared refresh pipeline. It parses
both surface configs once per refresh, warns about invalid items once
per session, synchronizes the git-branch cache, then renders each
surface from the same `StatusSurfaceSelections` snapshot.
4. **`chatwidget.rs`** sets `TerminalTitleStatusKind` at each state
transition (Working, Thinking, Undoing, WaitingForBackgroundTerminal)
and calls `refresh_terminal_title()` whenever relevant state changes.
5. **`app.rs`** handles the three setup events (confirm/preview/cancel),
persists config via `ConfigEditsBuilder`, and clears the managed title
on `Drop`.
## Non-goals
- **Restoring the previous terminal title on exit.** There is no
portable way to read the terminal's current title, so `Drop` clears the
managed title rather than restoring it.
- **Sharing code between `tui` and `tui_app_server`.** The
implementation is a parallel copy, matching the existing pattern for the
status-line feature. Extracting a shared crate is future work.
## Tradeoffs
- **Duplicate code across crates.** The three core files
(`terminal_title.rs`, `title_setup.rs`, `status_surfaces.rs`) are
byte-for-byte copies from the classic `tui`. This was chosen for
consistency with the existing status-line port and to avoid coupling the
two crates at the dependency level. Future changes must be applied in
both places.
- **`status_surfaces.rs` is large (~660 lines).** It absorbs logic that
previously lived inline in `chatwidget.rs` (status-line refresh, git
branch management, project root discovery) plus all new terminal-title
logic. This consolidation trades file size for a single place where both
surfaces are coordinated.
- **Spinner scheduling on every refresh.** The terminal title spinner
(when active) schedules a frame every 100ms. This is the same pattern
the status-indicator spinner already uses; the overhead is a timer
registration, not a redraw.
## Architecture
```
/title command
-> SlashCommand::Title
-> open_terminal_title_setup()
-> TerminalTitleSetupView (MultiSelectPicker)
-> on_change: AppEvent::TerminalTitleSetupPreview -> preview_terminal_title()
-> on_confirm: AppEvent::TerminalTitleSetup -> ConfigEditsBuilder + setup_terminal_title()
-> on_cancel: AppEvent::TerminalTitleSetupCancelled -> cancel_terminal_title_setup()
Runtime title refresh:
state change (turn start, reasoning, undo, plan update, thread rename, ...)
-> set terminal_title_status_kind
-> refresh_terminal_title()
-> status_surface_selections() (parse configs, collect invalids)
-> refresh_terminal_title_from_selections()
-> terminal_title_value_for_item() for each configured item
-> assemble title string with separators
-> skip if identical to last_terminal_title (dedup OSC writes)
-> set_terminal_title() (sanitize + OSC 0 write)
-> schedule spinner frame if animating
Widget replacement:
replace_chat_widget_with_app_server_thread()
-> transfer last_terminal_title from old widget to new
-> avoids redundant OSC clear+rewrite on session switch
```
## Observability
- Invalid terminal-title item IDs in config emit a one-per-session
warning via `on_warning()` (gated by
`terminal_title_invalid_items_warned` `AtomicBool`).
- OSC write failures are logged at `tracing::debug` level.
- Config persistence failures are logged at `tracing::error` and
surfaced to the user via `add_error_message()`.
## Tests
- `terminal_title.rs`: 4 unit tests covering sanitization (control
chars, bidi codepoints, truncation) and OSC output format.
- `title_setup.rs`: 3 tests covering setup view snapshot rendering,
parse order preservation, and invalid-ID rejection.
- `chatwidget/tests.rs`: Updated test helpers with new fields; existing
tests continue to pass.
---------
Co-authored-by: Eric Traut <etraut@openai.com>
## Summary
Add a focused codex network proxy unit test for the denylist pattern
with wildcard in the middle `region*.some.malicious.tunnel.com`. This
does not change how existing code works, just ensure that behavior stays
the same and we got CI guards to guard existin behavior.
## Why
The managed Codex denylist update relies on this mid label glob form,
and the existing tests only covered exact hosts, `*.` subdomains, and
`**.` apex plus subdomains.
## Validation
`cargo test -p codex-network-proxy
compile_globset_supports_mid_label_wildcards`
`cargo test -p codex-network-proxy`
`./tools/argument-comment-lint/run-prebuilt-linter.sh -p
codex-network-proxy`
## Summary
- block git global options that can redirect config, repository, or
helper lookup from being auto-approved as safe
- share the unsafe global-option predicate across the Unix and Windows
git safety checks
- add regression coverage for inline and split forms, including `bash
-lc` and PowerShell wrappers
## Root cause
The Unix safe-command gate only rejected `-c` and `--config-env`, even
though the shared git parser already knew how to skip additional
pre-subcommand globals such as `--git-dir`, `--work-tree`,
`--exec-path`, `--namespace`, and `--super-prefix`. That let those
arguments slip through safe-command classification on otherwise
read-only git invocations and bypass approval. The Windows-specific
safe-command path had the same trust-boundary gap for git global
options.
## Why
`#[large_stack_test]` made the `apply_patch_cli` tests pass by giving
them more stack, but it did not address why those tests needed the extra
stack in the first place.
The real problem is the async state built by the `apply_patch_cli`
harness path. Those tests await three helper boundaries directly:
harness construction, turn submission, and apply-patch output
collection. If those helpers inline their full child futures, the test
future grows to include the whole harness startup and request/response
path.
This change replaces the workaround from #12768 with the same basic
approach used in #13429, but keeps the fix narrower: only the helper
boundaries awaited directly by `apply_patch_cli` stay boxed.
## What Changed
- removed `#[large_stack_test]` from
`core/tests/suite/apply_patch_cli.rs`
- restored ordinary `#[tokio::test(flavor = "multi_thread",
worker_threads = 2)]` annotations in that suite
- deleted the now-unused `codex-test-macros` crate and removed its
workspace wiring
- boxed only the three helper boundaries that the suite awaits directly:
- `apply_patch_harness_with(...)`
- `TestCodexHarness::submit(...)`
- `TestCodexHarness::apply_patch_output(...)`
- added comments at those boxed boundaries explaining why they remain
boxed
## Testing
- `cargo test -p codex-core --test all suite::apply_patch_cli --
--nocapture`
## References
- #12768
- #13429
## Summary
- enrich `codex.mcp.call` with `tool`, `connector_id`, and sanitized
`connector_name` for actual MCP executions
- record `codex.mcp.call.duration_ms` for actual MCP executions so
connector-level latency is visible in metrics
- keep skipped, blocked, declined, and cancelled paths on the plain
status-only `codex.mcp.call` counter
## Included Changes
- `codex-rs/core/src/mcp_tool_call.rs`: add connector-sliced MCP count
and duration metrics only for executed tool calls, while leaving
non-executed outcomes as status-only counts
- `codex-rs/core/src/mcp_tool_call_tests.rs`: cover metric tag shaping,
connector-name sanitization, and the new duration metric tags
## Testing
- `cargo test -p codex-core`
- `just fix -p codex-core`
- `just fmt`
## Notes
- `cargo test -p codex-core` still hits existing unrelated failures in
approvals-reviewer config tests and the sandboxed JS REPL `mktemp` test
- full workspace `cargo test` was not run
---------
Co-authored-by: Codex <noreply@openai.com>
## Symptoms
When `/review` ran through `tui_app_server`, the TUI could show
duplicate review content:
- the `>> Code review started: ... <<` banner appeared twice
- the final review body could also appear twice
## Problem
`tui_app_server` was treating review lifecycle items as renderable
content on more than one delivery path.
Specifically:
- `EnteredReviewMode` was rendered both when the item started and again
when it completed
- `ExitedReviewMode` rendered the review text itself, even though the
same review text was also delivered later as the assistant message item
That meant the same logical review event was committed into history
multiple times.
## Solution
Make review lifecycle items control state transitions only once, and
keep the final review body sourced from the assistant message item:
- render the review-start banner from the live `ItemStarted` path, while
still allowing replay to restore it once
- treat `ExitedReviewMode` as a mode-exit/finish-banner event instead of
rendering the review body from it
- preserve the existing assistant-message rendering path as the single
source of final review text
This PR partially rebase `unified_exec` on the `exec-server` and adapt
the `exec-server` accordingly.
## What changed in `exec-server`
1. Replaced the old "broadcast-driven; process-global" event model with
process-scoped session events. The goal is to be able to have dedicated
handler for each process.
2. Add to protocol contract to support explicit lifecycle status and
stream ordering:
- `WriteResponse` now returns `WriteStatus` (Accepted, UnknownProcess,
StdinClosed, Starting) instead of a bool.
- Added seq fields to output/exited notifications.
- Added terminal process/closed notification.
3. Demultiplexed remote notifications into per-process channels. Same as
for the event sys
4. Local and remote backends now both implement ExecBackend.
5. Local backend wraps internal process ID/operations into per-process
ExecProcess objects.
6. Remote backend registers a session channel before launch and
unregisters on failed launch.
## What changed in `unified_exec`
1. Added unified process-state model and backend-neutral process
wrapper. This will probably disappear in the future, but it makes it
easier to keep the work flowing on both side.
- `UnifiedExecProcess` now handles both local PTY sessions and remote
exec-server processes through a shared `ProcessHandle`.
- Added `ProcessState` to track has_exited, exit_code, and terminal
failure message consistently across backends.
2. Routed write and lifecycle handling through process-level methods.
## Some rationals
1. The change centralizes execution transport in exec-server while
preserving policy and orchestration ownership in core, avoiding
duplicated launch approval logic. This comes from internal discussion.
2. Session-scoped events remove coupling/cross-talk between processes
and make stream ordering and terminal state explicit (seq, closed,
failed).
3. The failure-path surfacing (remote launch failures, write failures,
transport disconnects) makes command tool output and cleanup behavior
deterministic
## Follow-ups:
* Unify the concept of thread ID behind an obfuscated struct
* FD handling
* Full zsh-fork compatibility
* Full network sandboxing compatibility
* Handle ws disconnection