## Why
The skills extension needs the resolved turn environments to build a
real per-turn `SkillListQuery`. The previous `TurnLifecycleContributor`
hook only had a turn id, so it could only seed a placeholder query and
never carry the executor authorities that executor-scoped skill routing
will need.
Moving catalog resolution onto `TurnInputContributor` puts the skills
extension on the same turn-preparation path that already has the
environment ids and working directories for the submitted turn, while
keeping the actual prompt injection work for follow-up changes.
## What changed
- switch `ext/skills` from `TurnLifecycleContributor` to
`TurnInputContributor`
- build `executor_authorities` from `TurnInputContext.environments` and
pass them through `SkillListQuery`
- keep storing the resolved catalog in `SkillsTurnState`, but drop the
placeholder query helper that no longer matches the real data flow
- update the extension TODOs to reflect that per-turn catalog resolution
now happens in the turn-input contributor, and that prompt/context
injection still needs to move later
## Testing
- Not run locally.
## Summary
Some permission profiles can encode filesystem reads that should remain
unavailable to the agent. Before this change, the model-visible context
and automatic approval review prompt summarized the effective
permissions as a legacy sandbox mode, which can omit permission-profile
filesystem entries from escalation decisions.
For example, a profile can grant workspace access while denying a
private subtree across every workspace root:
```toml
default_permissions = "restricted-workspace"
[permissions.restricted-workspace.workspace_roots]
"/Users/alice/project" = true
"/Users/alice/other-project" = true
[permissions.restricted-workspace.filesystem]
":minimal" = "read"
[permissions.restricted-workspace.filesystem.":workspace_roots"]
"." = "write"
"private" = "deny"
"private/**" = "deny"
```
The context window now describes the workspace roots and effective
filesystem side of the `PermissionProfile` directly, with deny entries
marked as non-escalatable:
```xml
<environment_context>
<cwd>/Users/alice/project</cwd>
<shell>zsh</shell>
<filesystem><workspace_roots><root>/Users/alice/project</root><root>/Users/alice/other-project</root></workspace_roots><permission_profile type="managed"><file_system type="restricted"><entry access="read"><special>:minimal</special></entry><entry access="write"><path>/Users/alice/project</path></entry><entry access="write"><path>/Users/alice/other-project</path></entry><entry access="deny" escalatable="false"><path>/Users/alice/project/private</path></entry><entry access="deny" escalatable="false"><path>/Users/alice/other-project/private</path></entry><entry access="deny" escalatable="false"><glob>/Users/alice/project/private/**</glob></entry><entry access="deny" escalatable="false"><glob>/Users/alice/other-project/private/**</glob></entry></file_system></permission_profile></filesystem>
</environment_context>
```
Managed requirements can impose the same kind of deny-read restriction:
```toml
[permissions.filesystem]
deny_read = [
"/Users/alice/project/private",
"/Users/alice/project/private/**",
]
```
The automatic approval review prompt also receives the parent turn's
denied-read context, so review decisions can account for the active
permission profile.
## What Changed
- Render the effective filesystem profile in `<environment_context>`,
including profile type, filesystem entries, workspace roots, and
non-escalatable deny entries.
- Persist effective `workspace_roots` in `TurnContextItem` so
resumed/replayed context does not have to bind `:workspace_roots`
through legacy `cwd` fallback.
- Add explicit permission instructions that denied reads are policy
restrictions, not escalation targets.
- Pass the parent turn's denied-read context into automatic approval
reviews.
- Add targeted coverage for prompt rendering, workspace-root
materialization, replay context, and review prompt context.
- Keep the prompt-context test expectations platform-aware so the same
filesystem rendering assertions pass on Unix and Windows paths.
## Testing
- `just test -p codex-core
context::environment_context::tests::serialize_environment_context_with_full_filesystem_profile`
- `just test -p codex-core
context::environment_context::tests::turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd`
- `just test -p codex-core
context::permissions_instructions::permissions_instructions_tests::builds_permissions_from_profile_with_denied_reads`
- `just fix -p codex-core`
I also attempted `just test -p codex-core`; the changed prompt-context
tests passed, but the full local run did not complete cleanly in this
sandboxed macOS environment due unrelated user-shell `CODEX_SANDBOX*`
expectations and integration-test timeouts.
## Why
`ContextualUserFragment` needs to be usable behind `dyn` for render-only
paths, but associated constants made the trait non-object-safe.
## What changed
- Replaced associated constants with trait methods so `dyn
ContextualUserFragment` can render fragments.
- Preserved the existing typed `T::matches_text(text)` registration
pattern via `type_markers()`.
- Kept default `render()` on the main trait so implementations only
provide role, markers, and body.
- Added unit coverage for rendering a `Box<dyn ContextualUserFragment>`.
## Verification
- `cargo test -p codex-core contextual_user_fragment_is_dyn_compatible`
- `just fix -p codex-core`
## Why
The model-visible `<network>` context currently repeats indentation and
a pair of XML tags for every allowed or denied domain. Large domain sets
spend a surprising amount of prompt budget on that scaffolding instead
of the actual policy values.
## What changed
- Render allowed domains as one comma-separated `<allowed>` value
instead of one element per domain.
- Render denied domains the same way.
- Keep the full allow/deny domain sets model-visible while updating the
serialization and settings-update coverage for the denser shape.
## Example
Before:
```xml
<network enabled="true">
<allowed>api.example.test</allowed>
<allowed>cdn.example.test</allowed>
<denied>blocked.example.test</denied>
</network>
```
After:
```xml
<network enabled="true"><allowed>api.example.test,cdn.example.test</allowed><denied>blocked.example.test</denied></network>
```
## Validation
- `cargo test -p codex-core environment_context`
- `cargo test -p codex-core
build_settings_update_items_emits_environment_item_for_network_changes`
- Ran a local `codex` session with a real network context containing 121
allowed domains and 42 denied domains, then inspected the raw prompt
with `raw_token_viewer_cli.py`. With the same domain set, the rendered
`<network>` section shrank from 7,175 characters across 161 lines to
3,666 characters on one line, and the containing environment-context
block fell from 6,428 tokens to 5,379 tokens.
## Summary
- make resolved turn environment shell metadata optional instead of
hard-coding bash
- render environment context shells from explicit environment metadata
when present, falling back to the existing session shell
- update environment context tests for inherited PowerShell-style
fallback and explicit per-environment shell override
## Testing
- Not run (not requested; formatted with `just fmt`).
Co-authored-by: Codex <noreply@openai.com>
## Why
This is a prep PR in the multi-environment process-tool stack. It
separates ownership/config cleanup from the behavior change that teaches
process tools to route by selected environment, so the follow-up PR can
focus on model-facing `environment_id` behavior.
## Stack
1. https://github.com/openai/codex/pull/20646 - `EnvironmentContext`
rendering for selected environments
2. https://github.com/openai/codex/pull/20669 - selected-environment
ownership and tool config prep (this PR)
3. https://github.com/openai/codex/pull/20647 - process-tool
`environment_id` routing
## What Changed
- keep the resolved turn environment list wrapped in
`ResolvedTurnEnvironments` through `TurnContext` instead of unwrapping
it back to a raw `Vec`
- add `TurnContext::resolve_path_against` so cwd-relative path
resolution has one shared helper
- replace the old tool config boolean with `ToolEnvironmentMode::{None,
Single, Multiple}`
## Testing
- Tests not run locally; this prep refactor is covered by GitHub CI for
the stack.
Co-authored-by: Codex <noreply@openai.com>
## Why
The model needs a way to see which environments are available during a
multi-environment turn without changing the legacy single-environment
prompt surface or pulling replay/persistence changes into the same
review.
## Stack
1. https://github.com/openai/codex/pull/20646 - `EnvironmentContext`
rendering for selected environments (this PR)
2. https://github.com/openai/codex/pull/20669 - selected-environment
ownership and tool config prep
3. https://github.com/openai/codex/pull/20647 - process-tool
`environment_id` routing
## What Changed
- extend `environment_context` so multi-environment turns render an
`<environments>` block with the selected environment ids and cwd values
- keep zero- and single-environment turns on the existing cwd-only
render path
- keep replay and persistence paths on the legacy surface for now so
this PR stays scoped to live prompt rendering
- add focused coverage in
`codex-rs/core/src/context/environment_context_tests.rs`
## Testing
- CI
---------
Co-authored-by: Codex <noreply@openai.com>