diff --git a/.agents/skills/add-agent/SKILL.md b/.agents/skills/add-agent/SKILL.md
index b4bf3b7..5f3af34 100644
--- a/.agents/skills/add-agent/SKILL.md
+++ b/.agents/skills/add-agent/SKILL.md
@@ -13,6 +13,7 @@ End-to-end checklist for making a new AI coding agent available via `--agent`.
Adding a new agent touches five layers: the agent module, `src/agent/mod.rs`, unit tests, integration tests (including extending the `image_tests!` macro), and the README. The existing agents are the canonical reference:
- **`claude`** — Claude Code CLI, curl installer, onboarding skip via `.claude.json`, agent-level network policy, anthropic+vertexai inference, skills at `/sandbox/.claude/skills`.
+- **`codex`** — OpenAI Codex CLI, Node.js + npm installer, openai inference, model via `.codex/config.toml`, endpoint via `OPENAI_BASE_URL` env var, no skills support.
- **`opencode`** — Opencode CLI, curl installer, per-inference config submodule pattern (one `configure()` per provider), all three inference providers, skills at `/sandbox/.opencode/skills`.
## Step 1 — choose the file structure
diff --git a/README.md b/README.md
index 6e2ee95..6b2f0f7 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ The tool assembles the image in layers — base image, agent installation, agent
| Agent | User settings | Auto-onboarding | Skills |
| ---------- | ------------- | --------------- | ------ |
| `claude` | Yes | Yes | Yes
`~/.claude/skills/` |
+| `codex` | Yes | N/A | N/A |
| `opencode` | Yes | N/A | Yes
`~/.opencode/skills/` |
### Agent × Inference Supported Features
@@ -55,6 +56,7 @@ The tool assembles the image in layers — base image, agent installation, agent
| `opencode` | `vertexai` | N/A | No
fixed endpoint | Yes
`model` in `.config/opencode/config.json` |
| `opencode` | `ollama` | Yes
Ollama provider config | Yes
`baseURL` in Ollama provider config | Yes
`model` in `.config/opencode/config.json` |
| `opencode` | `openai` | Yes if model or endpoint | Yes
`baseURL` in custom provider config | Yes
`model` in `.config/opencode/config.json` |
+| `codex` | `openai` | Yes if model | Yes
`ENV OPENAI_BASE_URL` | Yes
`model` in `.codex/config.toml` |
## Quick start
@@ -225,10 +227,12 @@ Pass `--agent` to install an agent into the image.
| Agent | Value | Description |
| ----------- | ---------- | ------------------------------ |
| Claude Code | `claude` | Anthropic's Claude Code CLI |
+| Codex | `codex` | OpenAI's Codex CLI agent |
| OpenCode | `opencode` | OpenCode AI coding agent |
```sh
openshell-image-builder --runtime podman --agent claude myimage:latest
+openshell-image-builder --runtime podman --agent codex myimage:latest
openshell-image-builder --runtime podman --agent opencode myimage:latest
```
@@ -242,7 +246,7 @@ You can pre-populate the sandbox home directory with settings files specific to
/agents//
```
-where `` is the directory described in [Configuring the base image](#configuring-the-base-image), and `` matches the value passed to `--agent` (`claude` or `opencode`).
+where `` is the directory described in [Configuring the base image](#configuring-the-base-image), and `` matches the value passed to `--agent` (`claude`, `codex`, or `opencode`).
All files and subdirectories are copied into `/sandbox/` (the sandbox user's home directory), owned by the `sandbox` user. The copy happens before the agent is installed, so the agent installer can create additional files on top without overwriting your settings.
@@ -288,7 +292,7 @@ Pass `--inference` to allow the agent to reach its LLM backend. This is separate
| Anthropic | `anthropic` | `claude`, `opencode` | Anthropic API (`api.anthropic.com`) |
| Vertex AI | `vertexai` | `claude`, `opencode` | Google Vertex AI (`oauth2.googleapis.com`, `aiplatform.googleapis.com`, `*-aiplatform.googleapis.com`) |
| Ollama | `ollama` | `opencode` | Local models on the host machine, reached via `host.openshell.internal:11434` |
-| OpenAI | `openai` | `opencode` | OpenAI API (`api.openai.com`), or any OpenAI-compatible endpoint via `--endpoint` |
+| OpenAI | `openai` | `codex`, `opencode` | OpenAI API (`api.openai.com`), or any OpenAI-compatible endpoint via `--endpoint` |
```sh
openshell-image-builder --runtime podman --agent claude --inference anthropic myimage:latest
@@ -296,6 +300,7 @@ openshell-image-builder --runtime podman --agent opencode --inference anthropic
openshell-image-builder --runtime podman --agent claude --inference vertexai myimage:latest
openshell-image-builder --runtime podman --agent opencode --inference vertexai myimage:latest
openshell-image-builder --runtime podman --agent opencode --inference ollama myimage:latest
+openshell-image-builder --runtime podman --agent codex --inference openai myimage:latest
openshell-image-builder --runtime podman --agent opencode --inference openai myimage:latest
```
@@ -310,6 +315,7 @@ Use `--endpoint` to override the inference provider's default URL — useful for
| `opencode` | `anthropic` | ✅ | Written to opencode config as `provider.anthropic.options.baseURL` |
| `opencode` | `vertexai` | ❌ | Rejected — Vertex AI has a proprietary fixed endpoint |
| `opencode` | `ollama` | ✅ | Written to opencode config as `provider.ollama.options.baseURL`; `localhost` in the URL is rewritten to `host.openshell.internal`; defaults to `http://host.openshell.internal:11434/v1` if omitted |
+| `codex` | `openai` | ✅ | Baked into the image as `ENV OPENAI_BASE_URL=` |
| `opencode` | `openai` | ✅ | When provided, opencode is configured to use a custom `@ai-sdk/openai-compatible` provider with `options.baseURL` set to the given URL |
```sh
@@ -345,6 +351,7 @@ Use `--model` to bake a default model into the image. The agent uses this model
| `opencode` | `anthropic` | Written to opencode config as top-level `"model"` field (can be combined with `--endpoint`) |
| `opencode` | `vertexai` | Written to opencode config as top-level `"model"` field |
| `opencode` | `ollama` | Written to opencode config as top-level `"model": "ollama/"` field; only the specified model is registered in the models map |
+| `codex` | `openai` | Written to `.codex/config.toml` as `model = ""` |
| `opencode` | `openai` | Written to opencode config as `"model": "openai/"` (native OpenAI) or `"model": "custom/"` (with `--endpoint`) |
```sh
@@ -413,7 +420,7 @@ The policy is built in four layers, merged in order:
1. **Base** ([`assets/policy.yaml`](assets/policy.yaml)) — general-purpose tooling: Git operations over HTTPS and the GitHub REST API via `gh`.
2. **Inference** (added by `--inference`) — LLM backend endpoints scoped to the agent binary. For example, `--inference anthropic` adds `api.anthropic.com` and `statsig.anthropic.com`; `--inference vertexai` adds `oauth2.googleapis.com` and `aiplatform.googleapis.com` (including the `*-aiplatform.googleapis.com` wildcard); `--inference ollama` adds `host.openshell.internal:11434` for local model access; `--inference openai` adds `api.openai.com` (or the custom endpoint host when `--endpoint` is used).
-3. **Agent** (added by `--agent`) — agent-specific endpoints. For example, `--agent claude` adds `platform.claude.com`, `raw.githubusercontent.com`, and the GitHub REST API for Claude's coding tools; `--agent opencode` adds `opencode.ai`, `registry.npmjs.org`, and `models.dev`.
+3. **Agent** (added by `--agent`) — agent-specific endpoints. For example, `--agent claude` adds `platform.claude.com`, `raw.githubusercontent.com`, and the GitHub REST API for Claude's coding tools; `--agent codex` adds `registry.npmjs.org` and the GitHub REST API; `--agent opencode` adds `opencode.ai`, `registry.npmjs.org`, and `models.dev`.
4. **Workspace** (added from `network.hosts` in `.kaiden/workspace.json` when `--with-workspace-config` is used) — user-defined hosts that any binary in standard PATH directories (`/bin`, `/usr/bin`, `/usr/local/bin`, `/sandbox/.local/bin`) and the agent binary (when present) may reach. See [Workspace network rules](#workspace-network-rules).
## Dev Container Features
@@ -485,6 +492,7 @@ During the build, each skill directory is copied into the agent's skills directo
| Agent | Skills directory |
| ---------- | ------------------------------ |
| `claude` | `/sandbox/.claude/skills/` |
+| `codex` | N/A (no skills support) |
| `opencode` | `/sandbox/.opencode/skills/` |
With `--agent claude` and `"skills": ["./my-skill"]`, the skill lands at `/sandbox/.claude/skills/my-skill/` in the image, owned by the `sandbox` user.
@@ -569,7 +577,7 @@ openshell-image-builder [OPTIONS]
| `` | Tag for the built image (e.g. `myimage:latest`) |
| `--runtime ` | Container CLI to use for building images (`podman`, `docker`, `container`) |
| `--config ` | Path to config directory containing `config.toml` (env: `OPENSHELL_IMAGE_BUILDER_CONFIG`) |
-| `--agent ` | Agent to install in the image (`claude`, `opencode`) |
+| `--agent ` | Agent to install in the image (`claude`, `codex`, `opencode`) |
| `--inference ` | Inference server the agent will connect to (`anthropic`, `vertexai`, `ollama`, `openai`) |
| `--endpoint ` | Override the inference provider's default endpoint URL (see [Custom endpoint](#custom-endpoint---endpoint)) |
| `--model ` | Default model for the agent to use (see [Default model](#default-model---model)) |
@@ -695,6 +703,41 @@ $ openshell sandbox create \
-- bash -c 'cd /sandbox/work && claude --bare'
```
+### Codex agent + OpenAI models provider
+
+```sh
+$ openshell-image-builder \
+ --runtime podman \
+ --agent codex \
+ --inference openai \
+ --model o4-mini \
+ --with-agent-settings \
+ sandbox_image:codex_openai
+
+$ openshell provider create \
+ --type generic \
+ --credential OPENAI_API_KEY=sk-... \
+ --name codex_openai_provider
+
+$ openshell sandbox create \
+ --from sandbox_image:codex_openai \
+ --provider codex_openai_provider \
+ --upload . \
+ --name codex_openai_sandbox \
+ --no-auto-providers \
+ -- codex
+
+# Or, with podman driver, you can mount the files
+# (https://docs.nvidia.com/openshell/reference/sandbox-compute-drivers#podman-driver-config-mounts)
+$ openshell sandbox create \
+ --from sandbox_image:codex_openai \
+ --provider codex_openai_provider \
+ --driver-config-json '{"podman":{"mounts":[{"type":"bind","source":"/path/to/your/sources","target":"/sandbox/work","read_only":false}]}}' \
+ --name codex_openai_sandbox \
+ --no-auto-providers \
+ -- codex
+```
+
### OpenCode agent + Ollama (local models)
Ollama must be running on the host before starting the sandbox.
diff --git a/src/agent/codex.rs b/src/agent/codex.rs
new file mode 100644
index 0000000..a928158
--- /dev/null
+++ b/src/agent/codex.rs
@@ -0,0 +1,85 @@
+// Copyright (C) 2026 Red Hat, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+use super::Agent;
+
+pub struct CodexAgent;
+
+impl Agent for CodexAgent {
+ fn id(&self) -> &str {
+ "codex"
+ }
+
+ fn install(&self) -> String {
+ "RUN curl -fsSL https://chatgpt.com/codex/install.sh | sh\nENV PATH=/sandbox/.local/bin:$PATH"
+ .to_string()
+ }
+
+ fn binary_path(&self) -> &str {
+ "/sandbox/.local/bin/codex"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn agent_id_is_codex() {
+ assert_eq!(CodexAgent.id(), "codex");
+ }
+
+ #[test]
+ fn install_is_nonempty() {
+ assert!(!CodexAgent.install().is_empty());
+ }
+
+ #[test]
+ fn install_contains_codex_installer() {
+ assert!(
+ CodexAgent
+ .install()
+ .contains("https://chatgpt.com/codex/install.sh")
+ );
+ }
+
+ #[test]
+ fn install_adds_local_bin_to_path() {
+ assert!(
+ CodexAgent
+ .install()
+ .contains("ENV PATH=/sandbox/.local/bin:$PATH")
+ );
+ }
+
+ #[test]
+ fn binary_path_is_local_bin_codex() {
+ assert_eq!(CodexAgent.binary_path(), "/sandbox/.local/bin/codex");
+ }
+
+ #[test]
+ fn skills_dir_is_empty() {
+ assert_eq!(CodexAgent.skills_dir(), "");
+ }
+
+ #[test]
+ fn skip_onboarding_is_noop() {
+ let mut files = std::collections::HashMap::new();
+ files.insert("some.json".to_string(), "content".to_string());
+ let result = CodexAgent.skip_onboarding(files.clone());
+ assert_eq!(result, files);
+ }
+}
diff --git a/src/agent/mod.rs b/src/agent/mod.rs
index 18a1d4e..d800c3d 100644
--- a/src/agent/mod.rs
+++ b/src/agent/mod.rs
@@ -15,11 +15,14 @@
// SPDX-License-Identifier: Apache-2.0
mod claude;
+mod codex;
mod opencode;
#[cfg(test)]
pub use claude::ClaudeAgent;
#[cfg(test)]
+pub use codex::CodexAgent;
+#[cfg(test)]
pub use opencode::OpencodeAgent;
use clap::ValueEnum;
@@ -72,12 +75,14 @@ pub trait Agent {
#[derive(Clone, ValueEnum)]
pub enum AgentKind {
Claude,
+ Codex,
Opencode,
}
pub fn from_kind(kind: AgentKind) -> Box {
match kind {
AgentKind::Claude => Box::new(claude::ClaudeAgent),
+ AgentKind::Codex => Box::new(codex::CodexAgent),
AgentKind::Opencode => Box::new(opencode::OpencodeAgent),
}
}
@@ -98,6 +103,16 @@ mod tests {
assert!(agent.install().contains("https://opencode.ai/install"));
}
+ #[test]
+ fn from_kind_codex_installs_codex() {
+ let agent = from_kind(AgentKind::Codex);
+ assert!(
+ agent
+ .install()
+ .contains("https://chatgpt.com/codex/install.sh")
+ );
+ }
+
#[test]
fn opencode_skip_onboarding_is_noop() {
let agent = from_kind(AgentKind::Opencode);
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index e48822b..f6fcff8 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -112,6 +112,10 @@ static UBUNTU_OPENCODE_OPENAI_IMAGE: OnceLock = OnceLock::new();
static FEDORA_OPENCODE_OPENAI_IMAGE: OnceLock = OnceLock::new();
static UBI_OPENCODE_OPENAI_IMAGE: OnceLock = OnceLock::new();
static HUMMINGBIRD_OPENCODE_OPENAI_IMAGE: OnceLock = OnceLock::new();
+static UBUNTU_CODEX_IMAGE: OnceLock = OnceLock::new();
+static FEDORA_CODEX_IMAGE: OnceLock = OnceLock::new();
+static UBI_CODEX_IMAGE: OnceLock = OnceLock::new();
+static HUMMINGBIRD_CODEX_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_OPENCODE_OPENAI_MODEL_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_NO_POLICY_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_CLAUDE_NO_AGENT_SETTINGS_IMAGE: OnceLock = OnceLock::new();
@@ -625,6 +629,15 @@ fn check_opencode_in_path(image: &str, expected: bool) {
}
}
+fn check_codex_in_path(image: &str, expected: bool) {
+ let out = run_in_image(image, "which codex");
+ if expected {
+ assert!(out.status.success(), "codex not found in PATH");
+ } else {
+ assert!(!out.status.success(), "codex should not be in PATH");
+ }
+}
+
fn check_claude_policy(image: &str, expected: bool) {
let out = run_in_image(image, "cat /etc/openshell/policy.yaml");
assert!(out.status.success(), "failed to read policy.yaml");
@@ -659,6 +672,23 @@ fn check_opencode_policy(image: &str, expected: bool) {
}
}
+fn check_codex_policy(image: &str, expected: bool) {
+ let out = run_in_image(image, "cat /etc/openshell/policy.yaml");
+ assert!(out.status.success(), "failed to read policy.yaml");
+ let policy = String::from_utf8_lossy(&out.stdout);
+ if expected {
+ assert!(
+ policy.contains("name: codex"),
+ "codex policy rule not found in policy.yaml"
+ );
+ } else {
+ assert!(
+ !policy.contains("name: codex"),
+ "codex policy rule should not be present in policy.yaml"
+ );
+ }
+}
+
fn check_anthropic_policy(image: &str, expected: bool) {
let out = run_in_image(image, "cat /etc/openshell/policy.yaml");
assert!(out.status.success(), "failed to read policy.yaml");
@@ -732,7 +762,7 @@ fn check_openai_policy(image: &str, expected: bool) {
// ---------------------------------------------------------------------------
macro_rules! image_tests {
- ($mod_name:ident, $image_fn:ident, has_claude: $has_claude:literal, has_opencode: $has_opencode:literal, has_anthropic: $has_anthropic:literal, has_vertexai: $has_vertexai:literal, has_ollama: $has_ollama:literal, has_openai: $has_openai:literal) => {
+ ($mod_name:ident, $image_fn:ident, has_claude: $has_claude:literal, has_opencode: $has_opencode:literal, has_codex: $has_codex:literal, has_anthropic: $has_anthropic:literal, has_vertexai: $has_vertexai:literal, has_ollama: $has_ollama:literal, has_openai: $has_openai:literal) => {
mod $mod_name {
use super::*;
@@ -766,6 +796,12 @@ macro_rules! image_tests {
check_opencode_in_path($image_fn(), $has_opencode);
}
+ #[test]
+ #[ignore]
+ fn codex_in_path() {
+ check_codex_in_path($image_fn(), $has_codex);
+ }
+
#[test]
#[ignore]
fn policy_yaml_present() {
@@ -784,6 +820,12 @@ macro_rules! image_tests {
check_opencode_policy($image_fn(), $has_opencode);
}
+ #[test]
+ #[ignore]
+ fn policy_has_codex_rules() {
+ check_codex_policy($image_fn(), $has_codex);
+ }
+
#[test]
#[ignore]
fn policy_has_anthropic_rules() {
@@ -811,34 +853,38 @@ macro_rules! image_tests {
};
}
-image_tests!(ubuntu, ubuntu_image, has_claude: false, has_opencode: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(ubuntu_claude, ubuntu_claude_image, has_claude: true, has_opencode: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(ubuntu_opencode, ubuntu_opencode_image, has_claude: false, has_opencode: true, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(ubuntu_claude_vertexai, ubuntu_claude_vertexai_image, has_claude: true, has_opencode: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(ubuntu_opencode_vertexai,ubuntu_opencode_vertexai_image,has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(fedora, fedora_image, has_claude: false, has_opencode: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(fedora_claude, fedora_claude_image, has_claude: true, has_opencode: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(fedora_opencode, fedora_opencode_image, has_claude: false, has_opencode: true, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(fedora_claude_vertexai, fedora_claude_vertexai_image, has_claude: true, has_opencode: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(fedora_opencode_vertexai,fedora_opencode_vertexai_image,has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(ubi, ubi_image, has_claude: false, has_opencode: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(ubi_claude, ubi_claude_image, has_claude: true, has_opencode: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(ubi_opencode, ubi_opencode_image, has_claude: false, has_opencode: true, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(ubi_claude_vertexai, ubi_claude_vertexai_image, has_claude: true, has_opencode: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(ubi_opencode_vertexai, ubi_opencode_vertexai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(hummingbird, hummingbird_image, has_claude: false, has_opencode: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(hummingbird_claude, hummingbird_claude_image, has_claude: true, has_opencode: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(hummingbird_opencode, hummingbird_opencode_image, has_claude: false, has_opencode: true, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
-image_tests!(hummingbird_claude_vertexai, hummingbird_claude_vertexai_image, has_claude: true, has_opencode: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(hummingbird_opencode_vertexai, hummingbird_opencode_vertexai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
-image_tests!(ubuntu_opencode_ollama, ubuntu_opencode_ollama_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
-image_tests!(fedora_opencode_ollama, fedora_opencode_ollama_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
-image_tests!(ubi_opencode_ollama, ubi_opencode_ollama_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
-image_tests!(hummingbird_opencode_ollama, hummingbird_opencode_ollama_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
-image_tests!(ubuntu_opencode_openai, ubuntu_opencode_openai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
-image_tests!(fedora_opencode_openai, fedora_opencode_openai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
-image_tests!(ubi_opencode_openai, ubi_opencode_openai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
-image_tests!(hummingbird_opencode_openai, hummingbird_opencode_openai_image, has_claude: false, has_opencode: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(ubuntu, ubuntu_image, has_claude: false, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(ubuntu_claude, ubuntu_claude_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(ubuntu_opencode, ubuntu_opencode_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(ubuntu_claude_vertexai, ubuntu_claude_vertexai_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(ubuntu_opencode_vertexai,ubuntu_opencode_vertexai_image,has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(fedora, fedora_image, has_claude: false, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(fedora_claude, fedora_claude_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(fedora_opencode, fedora_opencode_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(fedora_claude_vertexai, fedora_claude_vertexai_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(fedora_opencode_vertexai,fedora_opencode_vertexai_image,has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(ubi, ubi_image, has_claude: false, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(ubi_claude, ubi_claude_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(ubi_opencode, ubi_opencode_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(ubi_claude_vertexai, ubi_claude_vertexai_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(ubi_opencode_vertexai, ubi_opencode_vertexai_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(hummingbird, hummingbird_image, has_claude: false, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(hummingbird_claude, hummingbird_claude_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(hummingbird_opencode, hummingbird_opencode_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: true, has_vertexai: false, has_ollama: false, has_openai: false);
+image_tests!(hummingbird_claude_vertexai, hummingbird_claude_vertexai_image, has_claude: true, has_opencode: false, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(hummingbird_opencode_vertexai, hummingbird_opencode_vertexai_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: true, has_ollama: false, has_openai: false);
+image_tests!(ubuntu_opencode_ollama, ubuntu_opencode_ollama_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
+image_tests!(fedora_opencode_ollama, fedora_opencode_ollama_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
+image_tests!(ubi_opencode_ollama, ubi_opencode_ollama_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
+image_tests!(hummingbird_opencode_ollama, hummingbird_opencode_ollama_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: true, has_openai: false);
+image_tests!(ubuntu_opencode_openai, ubuntu_opencode_openai_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(fedora_opencode_openai, fedora_opencode_openai_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(ubi_opencode_openai, ubi_opencode_openai_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(hummingbird_opencode_openai, hummingbird_opencode_openai_image, has_claude: false, has_opencode: true, has_codex: false, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(ubuntu_codex, ubuntu_codex_image, has_claude: false, has_opencode: false, has_codex: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(fedora_codex, fedora_codex_image, has_claude: false, has_opencode: false, has_codex: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(ubi_codex, ubi_codex_image, has_claude: false, has_opencode: false, has_codex: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
+image_tests!(hummingbird_codex, hummingbird_codex_image, has_claude: false, has_opencode: false, has_codex: true, has_anthropic: false, has_vertexai: false, has_ollama: false, has_openai: true);
// ---------------------------------------------------------------------------
// Workspace helpers for feature-based builds
@@ -1358,6 +1404,69 @@ fn hummingbird_opencode_openai_image() -> &'static str {
})
}
+fn ubuntu_codex_image() -> &'static str {
+ UBUNTU_CODEX_IMAGE.get_or_init(|| {
+ build_image(
+ "openshell-test-ubuntu-codex:integration",
+ &["--agent", "codex", "--inference", "openai", "--with-policy"],
+ )
+ })
+}
+
+fn fedora_codex_image() -> &'static str {
+ FEDORA_CODEX_IMAGE.get_or_init(|| {
+ let config = fedora_config_dir();
+ build_image(
+ "openshell-test-fedora-codex:integration",
+ &[
+ "--config",
+ config.path().to_str().unwrap(),
+ "--agent",
+ "codex",
+ "--inference",
+ "openai",
+ "--with-policy",
+ ],
+ )
+ })
+}
+
+fn ubi_codex_image() -> &'static str {
+ UBI_CODEX_IMAGE.get_or_init(|| {
+ let config = ubi_config_dir();
+ build_image(
+ "openshell-test-ubi-codex:integration",
+ &[
+ "--config",
+ config.path().to_str().unwrap(),
+ "--agent",
+ "codex",
+ "--inference",
+ "openai",
+ "--with-policy",
+ ],
+ )
+ })
+}
+
+fn hummingbird_codex_image() -> &'static str {
+ HUMMINGBIRD_CODEX_IMAGE.get_or_init(|| {
+ let config = hummingbird_config_dir();
+ build_image(
+ "openshell-test-hummingbird-codex:integration",
+ &[
+ "--config",
+ config.path().to_str().unwrap(),
+ "--agent",
+ "codex",
+ "--inference",
+ "openai",
+ "--with-policy",
+ ],
+ )
+ })
+}
+
fn ubuntu_opencode_openai_model_image() -> &'static str {
UBUNTU_OPENCODE_OPENAI_MODEL_IMAGE.get_or_init(|| {
build_image(
@@ -2090,6 +2199,58 @@ mod opencode_openai {
}
}
+// ---------------------------------------------------------------------------
+// Codex + OpenAI integration tests
+// ---------------------------------------------------------------------------
+
+mod codex_openai {
+ use super::*;
+
+ #[test]
+ #[ignore]
+ fn codex_with_anthropic_inference_is_rejected() {
+ let binary = env!("CARGO_BIN_EXE_openshell-image-builder");
+ let status = Command::new(binary)
+ .args([
+ "--runtime",
+ "podman",
+ "--agent",
+ "codex",
+ "--inference",
+ "anthropic",
+ "should-not-be-built:test",
+ ])
+ .status()
+ .expect("binary should run");
+ assert!(
+ !status.success(),
+ "building with --agent codex --inference anthropic should fail"
+ );
+ }
+
+ #[test]
+ #[ignore]
+ fn codex_with_ollama_inference_is_rejected() {
+ let binary = env!("CARGO_BIN_EXE_openshell-image-builder");
+ let status = Command::new(binary)
+ .args([
+ "--runtime",
+ "podman",
+ "--agent",
+ "codex",
+ "--inference",
+ "ollama",
+ "should-not-be-built:test",
+ ])
+ .status()
+ .expect("binary should run");
+ assert!(
+ !status.success(),
+ "building with --agent codex --inference ollama should fail"
+ );
+ }
+}
+
// ---------------------------------------------------------------------------
// --model integration tests
// ---------------------------------------------------------------------------
@@ -2102,6 +2263,7 @@ static UBUNTU_CLAUDE_ANTHROPIC_MODEL_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_CLAUDE_VERTEXAI_MODEL_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_OPENCODE_ANTHROPIC_MODEL_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_OPENCODE_OLLAMA_MODEL_IMAGE: OnceLock = OnceLock::new();
+static UBUNTU_CODEX_OPENAI_MODEL_IMAGE: OnceLock = OnceLock::new();
fn ubuntu_claude_anthropic_model_image() -> &'static str {
UBUNTU_CLAUDE_ANTHROPIC_MODEL_IMAGE.get_or_init(|| {
@@ -2171,6 +2333,23 @@ fn ubuntu_opencode_ollama_model_image() -> &'static str {
})
}
+fn ubuntu_codex_openai_model_image() -> &'static str {
+ UBUNTU_CODEX_OPENAI_MODEL_IMAGE.get_or_init(|| {
+ build_image(
+ "openshell-test-ubuntu-codex-openai-model:integration",
+ &[
+ "--agent",
+ "codex",
+ "--inference",
+ "openai",
+ "--model",
+ MODEL_OPENAI,
+ "--with-agent-settings",
+ ],
+ )
+ })
+}
+
// claude + anthropic + model: .claude/settings.json written with "model" field
mod model_claude_anthropic {
use super::*;
@@ -2415,6 +2594,50 @@ mod model_opencode_openai {
}
}
+// codex + openai + model: .codex/config.toml written with "model" field
+mod model_codex_openai {
+ use super::*;
+
+ #[test]
+ #[ignore]
+ fn config_toml_present() {
+ let out = run_in_image(
+ ubuntu_codex_openai_model_image(),
+ "test -f /sandbox/.codex/config.toml",
+ );
+ assert!(
+ out.status.success(),
+ ".codex/config.toml not found when built with --inference openai --model"
+ );
+ }
+
+ #[test]
+ #[ignore]
+ fn config_toml_contains_model() {
+ let cmd = format!("grep -q '{}' /sandbox/.codex/config.toml", MODEL_OPENAI);
+ let out = run_in_image(ubuntu_codex_openai_model_image(), &cmd);
+ assert!(
+ out.status.success(),
+ "model value not found in .codex/config.toml"
+ );
+ }
+
+ #[test]
+ #[ignore]
+ fn config_toml_owned_by_sandbox() {
+ let out = run_in_image(
+ ubuntu_codex_openai_model_image(),
+ "stat -c '%U' /sandbox/.codex/config.toml",
+ );
+ assert!(out.status.success(), "failed to stat .codex/config.toml");
+ assert_eq!(
+ String::from_utf8_lossy(&out.stdout).trim(),
+ "sandbox",
+ ".codex/config.toml not owned by sandbox"
+ );
+ }
+}
+
// ---------------------------------------------------------------------------
// --endpoint integration tests
// ---------------------------------------------------------------------------
@@ -2425,6 +2648,7 @@ const OLLAMA_CUSTOM_ENDPOINT: &str = "http://localhost:9999/v1";
static UBUNTU_CLAUDE_ANTHROPIC_ENDPOINT_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_OPENCODE_ANTHROPIC_ENDPOINT_IMAGE: OnceLock = OnceLock::new();
static UBUNTU_OPENCODE_OLLAMA_CUSTOM_ENDPOINT_IMAGE: OnceLock = OnceLock::new();
+static UBUNTU_CODEX_OPENAI_ENDPOINT_IMAGE: OnceLock = OnceLock::new();
fn ubuntu_claude_anthropic_endpoint_image() -> &'static str {
UBUNTU_CLAUDE_ANTHROPIC_ENDPOINT_IMAGE.get_or_init(|| {
@@ -2479,6 +2703,23 @@ fn ubuntu_opencode_ollama_custom_endpoint_image() -> &'static str {
})
}
+fn ubuntu_codex_openai_endpoint_image() -> &'static str {
+ UBUNTU_CODEX_OPENAI_ENDPOINT_IMAGE.get_or_init(|| {
+ build_image(
+ "openshell-test-ubuntu-codex-openai-endpoint:integration",
+ &[
+ "--agent",
+ "codex",
+ "--inference",
+ "openai",
+ "--endpoint",
+ "https://my-openai-proxy.example.com",
+ "--with-policy",
+ ],
+ )
+ })
+}
+
// claude + anthropic + custom endpoint: policy replaced, ANTHROPIC_BASE_URL baked in
mod endpoint_claude_anthropic {
use super::*;
@@ -2605,6 +2846,22 @@ mod endpoint_opencode_ollama_custom {
}
}
+// codex + openai + custom endpoint: OPENAI_BASE_URL baked in
+mod endpoint_codex_openai {
+ use super::*;
+
+ #[test]
+ #[ignore]
+ fn openai_base_url_env_set_to_proxy() {
+ let cmd = "test \"$OPENAI_BASE_URL\" = \"https://my-openai-proxy.example.com\"";
+ let out = run_in_image(ubuntu_codex_openai_endpoint_image(), cmd);
+ assert!(
+ out.status.success(),
+ "OPENAI_BASE_URL is not set to the proxy URL in the image"
+ );
+ }
+}
+
// vertexai + endpoint rejection: does not require podman, never #[ignore]
mod endpoint_rejection {
use super::*;
@@ -2860,6 +3117,12 @@ fn cleanup_images() {
"openshell-test-fedora-ssl-certs:integration",
"openshell-test-ubuntu-no-certs:integration",
"openshell-test-fedora-no-certs:integration",
+ "openshell-test-ubuntu-codex:integration",
+ "openshell-test-fedora-codex:integration",
+ "openshell-test-ubi-codex:integration",
+ "openshell-test-hummingbird-codex:integration",
+ "openshell-test-ubuntu-codex-openai-model:integration",
+ "openshell-test-ubuntu-codex-openai-endpoint:integration",
] {
Command::new("podman")
.args(["rmi", "--force", tag])