feat: codex qol updates (#348)
## Description - Removed variables for hardcoded configuration options, and replaced with variables for base config, and additional mcp servers. - Set module defaults so that this will run with minimal module configuration for tasks, while allowing further configuration if needed by the user for codex through the base configuration. - Updated tests for expected responses and new configuration options. - Move all codex related files outside of project folder. <!-- Briefly describe what this PR does and why --> ## Type of Change - [ ] New module - [ ] Bug fix - [X] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information <!-- Delete this section if not applicable --> **Path:** `registry/coder-labs/modules/codex` **New version:** `v1.1.0` **Breaking change:** [X] Yes [ ] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun run fmt`) - [X] Changes tested locally ## Related Issues <!-- Link related issues or write "None" if not applicable --> --------- Co-authored-by: Atif Ali <atif@coder.com>
This commit is contained in:
parent
eca289be3a
commit
d6ae51fad0
@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
|||||||
```tf
|
```tf
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "1.0.1"
|
version = "2.0.0"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = var.openai_api_key
|
openai_api_key = var.openai_api_key
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
@ -25,29 +25,24 @@ module "codex" {
|
|||||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
|
- You must add the [Coder Login](https://registry.coder.com/modules/coder/coder-login) module to your template
|
||||||
- OpenAI API key for Codex access
|
- OpenAI API key for Codex access
|
||||||
|
|
||||||
## Usage Example
|
## Examples
|
||||||
|
|
||||||
- Simple usage Example:
|
### Run standalone
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "codex" {
|
module "codex" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "1.0.1"
|
version = "2.0.0"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = "..."
|
openai_api_key = "..."
|
||||||
codex_model = "o4-mini"
|
|
||||||
install_codex = true
|
|
||||||
codex_version = "latest"
|
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
codex_system_prompt = "You are a helpful coding assistant. Start every response with `Codex says:`"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Example usage with Tasks:
|
### Tasks integration
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
# This
|
|
||||||
data "coder_parameter" "ai_prompt" {
|
data "coder_parameter" "ai_prompt" {
|
||||||
type = "string"
|
type = "string"
|
||||||
name = "AI Prompt"
|
name = "AI Prompt"
|
||||||
@ -65,78 +60,74 @@ module "coder-login" {
|
|||||||
|
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
|
version = "2.0.0"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = "..."
|
openai_api_key = "..."
|
||||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
approval_policy = "never" # Full auto mode
|
|
||||||
|
# Custom configuration for full auto mode
|
||||||
|
base_config_toml = <<-EOT
|
||||||
|
approval_policy = "never"
|
||||||
|
preferred_auth_method = "apikey"
|
||||||
|
EOT
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> **Security Notice**: This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module in trusted environments and be aware of the security implications.
|
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||||
|
|
||||||
## How it Works
|
## How it Works
|
||||||
|
|
||||||
- **Install**: The module installs Codex CLI and sets up the environment
|
- **Install**: The module installs Codex CLI and sets up the environment
|
||||||
- **System Prompt**: If `codex_system_prompt` and `folder` are set, creates the directory (if needed) and writes the prompt to `AGENTS.md`
|
- **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory
|
||||||
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
|
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI
|
||||||
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
|
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
|
||||||
|
|
||||||
## Sandbox Configuration
|
## Configuration
|
||||||
|
|
||||||
The module automatically configures Codex with a secure sandbox that allows AI tasks to work effectively:
|
### Default Configuration
|
||||||
|
|
||||||
- **Sandbox Mode**: `workspace-write` - Allows Codex to read/write files in the specified `folder`
|
When no custom `base_config_toml` is provided, the module uses these secure defaults:
|
||||||
- **Approval Policy**: `on-request` - Codex asks for permission before performing potentially risky operations
|
|
||||||
- **Network Access**: Enabled within the workspace for package installation and API calls
|
|
||||||
|
|
||||||
### Customizing Sandbox Behavior
|
```toml
|
||||||
|
sandbox_mode = "workspace-write"
|
||||||
|
approval_policy = "never"
|
||||||
|
preferred_auth_method = "apikey"
|
||||||
|
|
||||||
You can customize the sandbox behavior using dedicated variables:
|
[sandbox_workspace_write]
|
||||||
|
network_access = true
|
||||||
#### **Using Dedicated Variables (Recommended)**
|
|
||||||
|
|
||||||
For most use cases, use the dedicated sandbox variables:
|
|
||||||
|
|
||||||
```tf
|
|
||||||
module "codex" {
|
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
|
||||||
# ... other variables ...
|
|
||||||
|
|
||||||
# Containerized environments (fixes Landlock errors)
|
|
||||||
sandbox_mode = "danger-full-access"
|
|
||||||
|
|
||||||
# Or for read-only mode
|
|
||||||
# sandbox_mode = "read-only"
|
|
||||||
|
|
||||||
# Or for full auto mode
|
|
||||||
# approval_policy = "never"
|
|
||||||
|
|
||||||
# Or disable network access
|
|
||||||
# network_access = false
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Using extra_codex_settings_toml (Advanced)**
|
### Custom Configuration
|
||||||
|
|
||||||
For advanced configuration or when you need to override multiple settings:
|
For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_servers`:
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
|
version = "2.0.0"
|
||||||
# ... other variables ...
|
# ... other variables ...
|
||||||
|
|
||||||
extra_codex_settings_toml = <<-EOT
|
# Override default configuration
|
||||||
# Any custom Codex configuration
|
base_config_toml = <<-EOT
|
||||||
model = "gpt-4"
|
sandbox_mode = "danger-full-access"
|
||||||
disable_response_storage = true
|
approval_policy = "never"
|
||||||
|
preferred_auth_method = "apikey"
|
||||||
|
EOT
|
||||||
|
|
||||||
|
# Add extra MCP servers
|
||||||
|
additional_mcp_servers = <<-EOT
|
||||||
|
[mcp_servers.GitHub]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||||
|
type = "stdio"
|
||||||
EOT
|
EOT
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The dedicated variables (`sandbox_mode`, `approval_policy`, `network_access`) are the recommended way to configure sandbox behavior. Use `extra_codex_settings_toml` only for advanced configuration that isn't covered by the dedicated variables.
|
> If no custom configuration is provided, the module uses secure defaults. The Coder MCP server is always included automatically. For containerized workspaces (Docker/Kubernetes), you may need `sandbox_mode = "danger-full-access"` to avoid permission issues. For advanced options, see [Codex config docs](https://github.com/openai/codex/blob/main/codex-rs/config.md).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@ -150,6 +141,6 @@ module "codex" {
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [OpenAI API Documentation](https://platform.openai.com/docs)
|
- [Codex CLI Documentation](https://github.com/openai/codex)
|
||||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||||
|
|||||||
@ -108,24 +108,25 @@ describe("codex", async () => {
|
|||||||
await expectAgentAPIStarted(id);
|
await expectAgentAPIStarted(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("codex-config-toml", async () => {
|
test("base-config-toml", async () => {
|
||||||
const settings = dedent`
|
const baseConfig = dedent`
|
||||||
[mcp_servers.CustomMCP]
|
sandbox_mode = "danger-full-access"
|
||||||
command = "/Users/jkmr/Documents/work/coder/coder_darwin_arm64"
|
approval_policy = "never"
|
||||||
args = ["exp", "mcp", "server", "app-status-slug=codex"]
|
preferred_auth_method = "apikey"
|
||||||
env = { "CODER_MCP_APP_STATUS_SLUG" = "codex", "CODER_MCP_AI_AGENTAPI_URL"= "http://localhost:3284" }
|
|
||||||
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
|
[custom_section]
|
||||||
enabled = true
|
new_feature = true
|
||||||
type = "stdio"
|
|
||||||
`.trim();
|
`.trim();
|
||||||
const { id } = await setup({
|
const { id } = await setup({
|
||||||
moduleVariables: {
|
moduleVariables: {
|
||||||
extra_codex_settings_toml: settings,
|
base_config_toml: baseConfig,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await execModuleScript(id);
|
await execModuleScript(id);
|
||||||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||||
expect(resp).toContain("[mcp_servers.CustomMCP]");
|
expect(resp).toContain("sandbox_mode = \"danger-full-access\"");
|
||||||
|
expect(resp).toContain("preferred_auth_method = \"apikey\"");
|
||||||
|
expect(resp).toContain("[custom_section]");
|
||||||
expect(resp).toContain("[mcp_servers.Coder]");
|
expect(resp).toContain("[mcp_servers.Coder]");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ describe("codex", async () => {
|
|||||||
id,
|
id,
|
||||||
"/home/coder/.codex-module/agentapi-start.log",
|
"/home/coder/.codex-module/agentapi-start.log",
|
||||||
);
|
);
|
||||||
expect(resp).toContain("openai_api_key provided !");
|
expect(resp).toContain("OpenAI API Key: Provided");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("pre-post-install-scripts", async () => {
|
test("pre-post-install-scripts", async () => {
|
||||||
@ -181,25 +182,106 @@ describe("codex", async () => {
|
|||||||
expect(resp).toContain(folder);
|
expect(resp).toContain(folder);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("additional-extensions", async () => {
|
test("additional-mcp-servers", async () => {
|
||||||
const additional = dedent`
|
const additional = dedent`
|
||||||
[mcp_servers.CustomMCP]
|
[mcp_servers.GitHub]
|
||||||
command = "/Users/jkmr/Documents/work/coder/coder_darwin_arm64"
|
command = "npx"
|
||||||
args = ["exp", "mcp", "server", "app-status-slug=codex"]
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
||||||
env = { "CODER_MCP_APP_STATUS_SLUG" = "codex", "CODER_MCP_AI_AGENTAPI_URL"= "http://localhost:3284" }
|
|
||||||
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
|
|
||||||
enabled = true
|
|
||||||
type = "stdio"
|
type = "stdio"
|
||||||
|
description = "GitHub integration"
|
||||||
|
|
||||||
|
[mcp_servers.FileSystem]
|
||||||
|
command = "npx"
|
||||||
|
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
|
||||||
|
type = "stdio"
|
||||||
|
description = "File system access"
|
||||||
`.trim();
|
`.trim();
|
||||||
const { id } = await setup({
|
const { id } = await setup({
|
||||||
moduleVariables: {
|
moduleVariables: {
|
||||||
additional_extensions: additional,
|
additional_mcp_servers: additional,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await execModuleScript(id);
|
await execModuleScript(id);
|
||||||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||||
expect(resp).toContain("[mcp_servers.CustomMCP]");
|
expect(resp).toContain("[mcp_servers.GitHub]");
|
||||||
|
expect(resp).toContain("[mcp_servers.FileSystem]");
|
||||||
expect(resp).toContain("[mcp_servers.Coder]");
|
expect(resp).toContain("[mcp_servers.Coder]");
|
||||||
|
expect(resp).toContain("GitHub integration");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("full-custom-config", async () => {
|
||||||
|
const baseConfig = dedent`
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
approval_policy = "untrusted"
|
||||||
|
preferred_auth_method = "chatgpt"
|
||||||
|
custom_setting = "test-value"
|
||||||
|
|
||||||
|
[advanced_settings]
|
||||||
|
timeout = 30000
|
||||||
|
debug = true
|
||||||
|
logging_level = "verbose"
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const additionalMCP = dedent`
|
||||||
|
[mcp_servers.CustomTool]
|
||||||
|
command = "/usr/local/bin/custom-tool"
|
||||||
|
args = ["--serve", "--port", "8080"]
|
||||||
|
type = "stdio"
|
||||||
|
description = "Custom development tool"
|
||||||
|
|
||||||
|
[mcp_servers.DatabaseMCP]
|
||||||
|
command = "python"
|
||||||
|
args = ["-m", "database_mcp_server"]
|
||||||
|
type = "stdio"
|
||||||
|
description = "Database query interface"
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const { id } = await setup({
|
||||||
|
moduleVariables: {
|
||||||
|
base_config_toml: baseConfig,
|
||||||
|
additional_mcp_servers: additionalMCP,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await execModuleScript(id);
|
||||||
|
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||||
|
|
||||||
|
// Check base config
|
||||||
|
expect(resp).toContain("sandbox_mode = \"read-only\"");
|
||||||
|
expect(resp).toContain("preferred_auth_method = \"chatgpt\"");
|
||||||
|
expect(resp).toContain("custom_setting = \"test-value\"");
|
||||||
|
expect(resp).toContain("[advanced_settings]");
|
||||||
|
expect(resp).toContain("logging_level = \"verbose\"");
|
||||||
|
|
||||||
|
// Check MCP servers
|
||||||
|
expect(resp).toContain("[mcp_servers.Coder]");
|
||||||
|
expect(resp).toContain("[mcp_servers.CustomTool]");
|
||||||
|
expect(resp).toContain("[mcp_servers.DatabaseMCP]");
|
||||||
|
expect(resp).toContain("Custom development tool");
|
||||||
|
expect(resp).toContain("Database query interface");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("minimal-default-config", async () => {
|
||||||
|
const { id } = await setup({
|
||||||
|
moduleVariables: {
|
||||||
|
// No base_config_toml or additional_mcp_servers - should use defaults
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await execModuleScript(id);
|
||||||
|
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
|
||||||
|
|
||||||
|
// Check default base config
|
||||||
|
expect(resp).toContain("sandbox_mode = \"workspace-write\"");
|
||||||
|
expect(resp).toContain("approval_policy = \"never\"");
|
||||||
|
expect(resp).toContain("[sandbox_workspace_write]");
|
||||||
|
expect(resp).toContain("network_access = true");
|
||||||
|
|
||||||
|
// Check only Coder MCP server is present
|
||||||
|
expect(resp).toContain("[mcp_servers.Coder]");
|
||||||
|
expect(resp).toContain("Report ALL tasks and statuses");
|
||||||
|
|
||||||
|
// Ensure no additional MCP servers
|
||||||
|
const mcpServerCount = (resp.match(/\[mcp_servers\./g) || []).length;
|
||||||
|
expect(mcpServerCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("codex-system-prompt", async () => {
|
test("codex-system-prompt", async () => {
|
||||||
@ -210,7 +292,7 @@ describe("codex", async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await execModuleScript(id);
|
await execModuleScript(id);
|
||||||
const resp = await readFileContainer(id, "/home/coder/AGENTS.md");
|
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
|
||||||
expect(resp).toContain(prompt);
|
expect(resp).toContain(prompt);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -223,7 +305,8 @@ describe("codex", async () => {
|
|||||||
`.trim();
|
`.trim();
|
||||||
const pre_install_script = dedent`
|
const pre_install_script = dedent`
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo -e "${prompt_3}" >> /home/coder/AGENTS.md
|
mkdir -p /home/coder/.codex
|
||||||
|
echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
const { id } = await setup({
|
const { id } = await setup({
|
||||||
@ -233,7 +316,7 @@ describe("codex", async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await execModuleScript(id);
|
await execModuleScript(id);
|
||||||
const resp = await readFileContainer(id, "/home/coder/AGENTS.md");
|
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
|
||||||
expect(resp).toContain(prompt_1);
|
expect(resp).toContain(prompt_1);
|
||||||
expect(resp).toContain(prompt_2);
|
expect(resp).toContain(prompt_2);
|
||||||
|
|
||||||
@ -245,7 +328,7 @@ describe("codex", async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await execModuleScript(id_2);
|
await execModuleScript(id_2);
|
||||||
const resp_2 = await readFileContainer(id_2, "/home/coder/AGENTS.md");
|
const resp_2 = await readFileContainer(id_2, "/home/coder/.codex/AGENTS.md");
|
||||||
expect(resp_2).toContain(prompt_1);
|
expect(resp_2).toContain(prompt_1);
|
||||||
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
|
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
|
||||||
expect(count).toBe(1);
|
expect(count).toBe(1);
|
||||||
@ -268,12 +351,16 @@ describe("codex", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("start-without-prompt", async () => {
|
test("start-without-prompt", async () => {
|
||||||
const { id } = await setup();
|
const { id } = await setup({
|
||||||
|
moduleVariables: {
|
||||||
|
codex_system_prompt: "", // Explicitly disable system prompt
|
||||||
|
},
|
||||||
|
});
|
||||||
await execModuleScript(id);
|
await execModuleScript(id);
|
||||||
const prompt = await execContainer(id, [
|
const prompt = await execContainer(id, [
|
||||||
"ls",
|
"ls",
|
||||||
"-l",
|
"-l",
|
||||||
"/home/coder/AGENTS.md",
|
"/home/coder/.codex/AGENTS.md",
|
||||||
]);
|
]);
|
||||||
expect(prompt.exitCode).not.toBe(0);
|
expect(prompt.exitCode).not.toBe(0);
|
||||||
expect(prompt.stderr).toContain("No such file or directory");
|
expect(prompt.stderr).toContain("No such file or directory");
|
||||||
|
|||||||
@ -53,41 +53,21 @@ variable "codex_version" {
|
|||||||
default = "" # empty string means the latest available version
|
default = "" # empty string means the latest available version
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "extra_codex_settings_toml" {
|
variable "base_config_toml" {
|
||||||
type = string
|
type = string
|
||||||
description = "Settings to append to ~/.codex/config.toml."
|
description = "Complete base TOML configuration for Codex (without mcp_servers section). If empty, uses minimal default configuration with workspace-write sandbox mode and never approval policy. For advanced options, see https://github.com/openai/codex/blob/main/codex-rs/config.md"
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "sandbox_mode" {
|
variable "additional_mcp_servers" {
|
||||||
type = string
|
type = string
|
||||||
description = "The sandbox mode for Codex. Options: workspace-write, read-only, danger-full-access."
|
description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section."
|
||||||
default = "workspace-write"
|
default = ""
|
||||||
validation {
|
|
||||||
condition = contains(["workspace-write", "read-only", "danger-full-access"], var.sandbox_mode)
|
|
||||||
error_message = "sandbox_mode must be one of: workspace-write, read-only, danger-full-access."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "approval_policy" {
|
|
||||||
type = string
|
|
||||||
description = "The approval policy for Codex. Options: on-request, never, untrusted."
|
|
||||||
default = "on-request"
|
|
||||||
validation {
|
|
||||||
condition = contains(["on-request", "never", "untrusted"], var.approval_policy)
|
|
||||||
error_message = "approval_policy must be one of: on-request, never, untrusted."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "network_access" {
|
|
||||||
type = bool
|
|
||||||
description = "Whether to allow network access in workspace-write mode."
|
|
||||||
default = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "openai_api_key" {
|
variable "openai_api_key" {
|
||||||
type = string
|
type = string
|
||||||
description = "Codex API Key"
|
description = "OpenAI API key for Codex CLI"
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +85,7 @@ variable "agentapi_version" {
|
|||||||
|
|
||||||
variable "codex_model" {
|
variable "codex_model" {
|
||||||
type = string
|
type = string
|
||||||
description = "The model for Codex to use (e.g., o4-mini)."
|
description = "The model for Codex to use. Defaults to gpt-5."
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,24 +103,16 @@ variable "post_install_script" {
|
|||||||
|
|
||||||
variable "ai_prompt" {
|
variable "ai_prompt" {
|
||||||
type = string
|
type = string
|
||||||
description = "Task prompt for the Codex CLI"
|
description = "Initial task prompt for Codex CLI when launched via Tasks"
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "additional_extensions" {
|
|
||||||
type = string
|
|
||||||
description = "Additional extensions configuration in json format to append to the config."
|
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "codex_system_prompt" {
|
variable "codex_system_prompt" {
|
||||||
type = string
|
type = string
|
||||||
description = "System prompt for Codex. It will be added to AGENTS.md in the specified folder."
|
description = "System instructions written to AGENTS.md in the ~/.codex directory"
|
||||||
default = ""
|
default = "You are a helpful coding assistant. Start every response with `Codex says:`"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
resource "coder_env" "openai_api_key" {
|
resource "coder_env" "openai_api_key" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "OPENAI_API_KEY"
|
name = "OPENAI_API_KEY"
|
||||||
@ -194,14 +166,11 @@ module "agentapi" {
|
|||||||
chmod +x /tmp/install.sh
|
chmod +x /tmp/install.sh
|
||||||
ARG_INSTALL='${var.install_codex}' \
|
ARG_INSTALL='${var.install_codex}' \
|
||||||
ARG_CODEX_VERSION='${var.codex_version}' \
|
ARG_CODEX_VERSION='${var.codex_version}' \
|
||||||
ARG_EXTRA_CODEX_CONFIG='${base64encode(var.extra_codex_settings_toml)}' \
|
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
|
||||||
|
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
|
||||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||||
ARG_ADDITIONAL_EXTENSIONS='${base64encode(var.additional_extensions)}' \
|
|
||||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||||
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
|
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
|
||||||
ARG_SANDBOX_MODE='${var.sandbox_mode}' \
|
|
||||||
ARG_APPROVAL_POLICY='${var.approval_policy}' \
|
|
||||||
ARG_NETWORK_ACCESS='${var.network_access}' \
|
|
||||||
/tmp/install.sh
|
/tmp/install.sh
|
||||||
EOT
|
EOT
|
||||||
}
|
}
|
||||||
@ -3,7 +3,6 @@ source "$HOME"/.bashrc
|
|||||||
|
|
||||||
BOLD='\033[0;1m'
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
# Function to check if a command exists
|
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$1" > /dev/null 2>&1
|
command -v "$1" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
@ -11,25 +10,23 @@ set -o errexit
|
|||||||
set -o pipefail
|
set -o pipefail
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
ARG_EXTRA_CODEX_CONFIG=$(echo -n "$ARG_EXTRA_CODEX_CONFIG" | base64 -d)
|
ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | base64 -d)
|
||||||
ARG_ADDITIONAL_EXTENSIONS=$(echo -n "$ARG_ADDITIONAL_EXTENSIONS" | base64 -d)
|
ARG_ADDITIONAL_MCP_SERVERS=$(echo -n "$ARG_ADDITIONAL_MCP_SERVERS" | base64 -d)
|
||||||
ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d)
|
ARG_CODEX_INSTRUCTION_PROMPT=$(echo -n "$ARG_CODEX_INSTRUCTION_PROMPT" | base64 -d)
|
||||||
|
|
||||||
echo "--------------------------------"
|
echo "=== Codex Module Configuration ==="
|
||||||
printf "install: %s\n" "$ARG_INSTALL"
|
printf "Install Codex: %s\n" "$ARG_INSTALL"
|
||||||
printf "codex_version: %s\n" "$ARG_CODEX_VERSION"
|
printf "Codex Version: %s\n" "$ARG_CODEX_VERSION"
|
||||||
printf "codex_config: %s\n" "$ARG_EXTRA_CODEX_CONFIG"
|
printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
|
||||||
printf "app_slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
|
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||||
printf "additional_extensions: %s\n" "$ARG_ADDITIONAL_EXTENSIONS"
|
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
|
||||||
printf "start_directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
|
||||||
printf "instruction_prompt: %s\n" "$ARG_CODEX_INSTRUCTION_PROMPT"
|
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
|
||||||
|
echo "======================================"
|
||||||
echo "--------------------------------"
|
|
||||||
|
|
||||||
set +o nounset
|
set +o nounset
|
||||||
|
|
||||||
function install_node() {
|
function install_node() {
|
||||||
# borrowed from claude-code module
|
|
||||||
if ! command_exists npm; then
|
if ! command_exists npm; then
|
||||||
printf "npm not found, checking for Node.js installation...\n"
|
printf "npm not found, checking for Node.js installation...\n"
|
||||||
if ! command_exists node; then
|
if ! command_exists node; then
|
||||||
@ -58,24 +55,18 @@ function install_node() {
|
|||||||
|
|
||||||
function install_codex() {
|
function install_codex() {
|
||||||
if [ "${ARG_INSTALL}" = "true" ]; then
|
if [ "${ARG_INSTALL}" = "true" ]; then
|
||||||
# we need node to install and run codex-cli
|
|
||||||
install_node
|
install_node
|
||||||
|
|
||||||
# If nvm does not exist, we will create a global npm directory (this os to prevent the possibility of EACCESS issues on npm -g)
|
|
||||||
if ! command_exists nvm; then
|
if ! command_exists nvm; then
|
||||||
printf "which node: %s\n" "$(which node)"
|
printf "which node: %s\n" "$(which node)"
|
||||||
printf "which npm: %s\n" "$(which npm)"
|
printf "which npm: %s\n" "$(which npm)"
|
||||||
|
|
||||||
# Create a directory for global packages
|
|
||||||
mkdir -p "$HOME"/.npm-global
|
mkdir -p "$HOME"/.npm-global
|
||||||
|
|
||||||
# Configure npm to use it
|
|
||||||
npm config set prefix "$HOME/.npm-global"
|
npm config set prefix "$HOME/.npm-global"
|
||||||
|
|
||||||
# Add to PATH for current session
|
|
||||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||||
|
|
||||||
# Add to shell profile for future sessions
|
|
||||||
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
|
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
|
||||||
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
|
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
|
||||||
fi
|
fi
|
||||||
@ -92,79 +83,82 @@ function install_codex() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function populate_config_toml() {
|
write_minimal_default_config() {
|
||||||
CONFIG_PATH="$HOME/.codex/config.toml"
|
local config_path="$1"
|
||||||
mkdir -p "$(dirname "$CONFIG_PATH")"
|
cat << EOF > "$config_path"
|
||||||
printf "Custom codex_config is provided !\n"
|
# Minimal Default Codex Configuration
|
||||||
BASE_SANDBOX_CONFIG=$(
|
sandbox_mode = "workspace-write"
|
||||||
cat << EOF
|
approval_policy = "never"
|
||||||
# Base sandbox configuration for Codex workspace access
|
preferred_auth_method = "apikey"
|
||||||
# This ensures Codex can read/write files in the specified folder for AI tasks
|
|
||||||
sandbox_mode = "${ARG_SANDBOX_MODE}"
|
|
||||||
approval_policy = "${ARG_APPROVAL_POLICY}"
|
|
||||||
|
|
||||||
# Allow network access in workspace-write mode for package installation, API calls, etc.
|
|
||||||
[sandbox_workspace_write]
|
[sandbox_workspace_write]
|
||||||
network_access = ${ARG_NETWORK_ACCESS}
|
network_access = true
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
BASE_EXTENSIONS=$(
|
EOF
|
||||||
cat << EOF
|
}
|
||||||
|
|
||||||
|
append_mcp_servers_section() {
|
||||||
|
local config_path="$1"
|
||||||
|
|
||||||
|
cat << EOF >> "$config_path"
|
||||||
|
|
||||||
|
# MCP Servers Configuration
|
||||||
[mcp_servers.Coder]
|
[mcp_servers.Coder]
|
||||||
command = "coder"
|
command = "coder"
|
||||||
args = ["exp", "mcp", "server"]
|
args = ["exp", "mcp", "server"]
|
||||||
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL"= "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
|
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
|
||||||
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
|
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
|
||||||
type = "stdio"
|
type = "stdio"
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
)
|
|
||||||
|
|
||||||
echo "
|
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
|
||||||
${BASE_SANDBOX_CONFIG}
|
printf "Adding additional MCP servers\n"
|
||||||
|
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
${ARG_EXTRA_CODEX_CONFIG}
|
function populate_config_toml() {
|
||||||
|
CONFIG_PATH="$HOME/.codex/config.toml"
|
||||||
|
mkdir -p "$(dirname "$CONFIG_PATH")"
|
||||||
|
|
||||||
${BASE_EXTENSIONS}
|
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
|
||||||
|
printf "Using provided base configuration\n"
|
||||||
${ARG_ADDITIONAL_EXTENSIONS}
|
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
|
||||||
" > "$HOME/.codex/config.toml"
|
else
|
||||||
|
printf "Using minimal default configuration\n"
|
||||||
|
write_minimal_default_config "$CONFIG_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
append_mcp_servers_section "$CONFIG_PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_instruction_prompt_if_exists() {
|
function add_instruction_prompt_if_exists() {
|
||||||
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
|
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
|
||||||
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
|
AGENTS_PATH="$HOME/.codex/AGENTS.md"
|
||||||
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
|
||||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
|
||||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
mkdir -p "$HOME/.codex"
|
||||||
exit 1
|
|
||||||
}
|
if [ -f "${AGENTS_PATH}" ] && grep -Fq "${ARG_CODEX_INSTRUCTION_PROMPT}" "${AGENTS_PATH}"; then
|
||||||
|
printf "AGENTS.md already contains the instruction prompt. Skipping append.\n"
|
||||||
else
|
else
|
||||||
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
printf "Appending instruction prompt to AGENTS.md in .codex directory\n"
|
||||||
|
echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> "${AGENTS_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${ARG_CODEX_START_DIRECTORY}" ]; then
|
||||||
|
printf "Creating start directory '%s'\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||||
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
|
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
|
||||||
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
cd "${ARG_CODEX_START_DIRECTORY}" || {
|
|
||||||
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if AGENTS.md contains the instruction prompt already
|
|
||||||
if [ -f AGENTS.md ] && grep -Fxq "${ARG_CODEX_INSTRUCTION_PROMPT}" AGENTS.md; then
|
|
||||||
printf "AGENTS.md already contains the instruction prompt. Skipping append.\n"
|
|
||||||
else
|
|
||||||
printf "Appending instruction prompt to AGENTS.md\n"
|
|
||||||
echo -e "\n${ARG_CODEX_INSTRUCTION_PROMPT}" >> AGENTS.md
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
printf "AGENTS.md is not set.\n"
|
printf "AGENTS.md instruction prompt is not set.\n"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install Codex
|
|
||||||
install_codex
|
install_codex
|
||||||
codex --version
|
codex --version
|
||||||
populate_config_toml
|
populate_config_toml
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Load shell environment
|
|
||||||
source "$HOME"/.bashrc
|
source "$HOME"/.bashrc
|
||||||
set -o errexit
|
set -o errexit
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
@ -18,12 +17,12 @@ printf "Version: %s\n" "$(codex --version)"
|
|||||||
set -o nounset
|
set -o nounset
|
||||||
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
|
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
|
||||||
|
|
||||||
echo "--------------------------------"
|
echo "=== Codex Launch Configuration ==="
|
||||||
printf "openai_api_key: %s\n" "$ARG_OPENAI_API_KEY"
|
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
|
||||||
printf "codex_model: %s\n" "$ARG_CODEX_MODEL"
|
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
|
||||||
printf "start_directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||||
printf "task_prompt: %s\n" "$ARG_CODEX_TASK_PROMPT"
|
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
|
||||||
echo "--------------------------------"
|
echo "======================================"
|
||||||
set +o nounset
|
set +o nounset
|
||||||
CODEX_ARGS=()
|
CODEX_ARGS=()
|
||||||
|
|
||||||
@ -66,13 +65,9 @@ else
|
|||||||
printf "No task prompt given.\n"
|
printf "No task prompt given.\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$ARG_OPENAI_API_KEY" ]; then
|
|
||||||
printf "openai_api_key provided !\n"
|
|
||||||
else
|
|
||||||
printf "openai_api_key not provided\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# use low width to fit in the tasks UI sidebar
|
# Terminal dimensions optimized for Coder Tasks UI sidebar:
|
||||||
# we adjust the height to 930 due to a bug in codex, see: https://github.com/openai/codex/issues/1608
|
# - Width 67: fits comfortably in sidebar
|
||||||
printf "Starting codex with %s\n" "${CODEX_ARGS[@]}"
|
# - Height 1190: adjusted due to Codex terminal height bug
|
||||||
|
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
|
||||||
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
|
agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user