-
Notifications
You must be signed in to change notification settings - Fork 43
feat: support codex cli #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5bbca30
be3bc0a
5ac3d7a
37f85c6
13b776d
4e45ffd
66bb40e
b08aadf
93b16cb
92e6bb5
77393b4
b33bbda
dc154d0
ee138e1
d32f5d9
d08a225
a4481f7
2b7d213
79a3f8e
1a28cd2
49eafc5
723cdc4
d4d9b73
d0846b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,100 @@ | ||||
--- | ||||
display_name: Codex CLI | ||||
icon: ../../../../.icons/openai.svg | ||||
description: Run Codex CLI in your workspace with AgentAPI integration | ||||
verified: true | ||||
tags: [agent, codex, ai, openai, tasks] | ||||
--- | ||||
|
||||
# Codex CLI | ||||
|
||||
Run Codex CLI in your workspace to access OpenAI's models through the Codex interface, with custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility. | ||||
|
||||
```tf | ||||
module "codex" { | ||||
source = "registry.coder.com/coder-labs/codex/coder" | ||||
version = "1.0.0" | ||||
agent_id = coder_agent.example.id | ||||
openai_api_key = var.openai_api_key | ||||
agentapi_version = "v0.3.2" | ||||
} | ||||
``` | ||||
|
||||
35C4n0r marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
## Prerequisites | ||||
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module to your template | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we instead use the module inside the module? I know we have the same requirement in other modules too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @matifali So, instead of the user adding this, we pre-include it in our module ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Needs to test though how it works. |
||||
- OpenAI API key for Codex access | ||||
|
||||
## Usage Example | ||||
|
||||
- Simple usage Example: | ||||
|
||||
```tf | ||||
module "codex" { | ||||
count = data.coder_workspace.me.start_count | ||||
source = "registry.coder.com/coder-labs/codex/coder" | ||||
version = "1.0.0" | ||||
agent_id = coder_agent.example.id | ||||
openai_api_key = "..." | ||||
codex_model = "o4-mini" | ||||
install_codex = true | ||||
codex_version = "latest" | ||||
folder = "/home/coder/project" | ||||
codex_system_prompt = "You are a helpful coding assistant. Start every response with `Codex says:`" | ||||
} | ||||
``` | ||||
|
||||
- Example usage with Tasks: | ||||
|
||||
```tf | ||||
# This | ||||
data "coder_parameter" "ai_prompt" { | ||||
type = "string" | ||||
name = "AI Prompt" | ||||
default = "" | ||||
description = "Initial prompt for the Codex CLI" | ||||
mutable = true | ||||
} | ||||
|
||||
module "coder-login" { | ||||
count = data.coder_workspace.me.start_count | ||||
source = "registry.coder.com/coder/coder-login/coder" | ||||
version = "1.0.31" | ||||
agent_id = coder_agent.example.id | ||||
} | ||||
|
||||
module "codex" { | ||||
source = "registry.coder.com/coder-labs/codex/coder" | ||||
agent_id = coder_agent.example.id | ||||
openai_api_key = "..." | ||||
ai_prompt = data.coder_parameter.ai_prompt.value | ||||
agentapi_version = "v0.3.2" | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
} | ||||
``` | ||||
|
||||
> **Security Notice**: This module uses the `--dangerously-bypass-approvals-and-sandbox` flag when running Codex CLI. This flag | ||||
> bypasses standard permission checks and allows Codex CLI broader access to your system than normally permitted. While | ||||
> this enables more functionality, it also means Codex CLI can potentially execute commands with the same privileges as | ||||
> the user running it. Use this module _only_ in trusted environments and be aware of the security implications. | ||||
|
||||
## How it Works | ||||
|
||||
- **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` | ||||
- **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI | ||||
- **Environment**: Sets `OPENAI_API_KEY` and `CODEX_MODEL` for the CLI (if variables provided) | ||||
|
||||
## Troubleshooting | ||||
|
||||
- Check installation and startup logs in `~/.codex-module/` | ||||
- Ensure your OpenAI API key has access to the specified model | ||||
|
||||
> [!IMPORTANT] | ||||
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker). | ||||
> The module automatically configures Codex with your API key and model preferences. | ||||
|
||||
## References | ||||
|
||||
- [OpenAI API Documentation](https://platform.openai.com/docs) | ||||
- [AgentAPI Documentation](https://github.com/coder/agentapi) | ||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
import { | ||
test, | ||
afterEach, | ||
describe, | ||
setDefaultTimeout, | ||
beforeAll, | ||
expect, | ||
} from "bun:test"; | ||
import { execContainer, readFileContainer, runTerraformInit } from "~test"; | ||
import { | ||
loadTestFile, | ||
writeExecutable, | ||
setup as setupUtil, | ||
execModuleScript, | ||
expectAgentAPIStarted, | ||
} from "../../../coder/modules/agentapi/test-util"; | ||
35C4n0r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import dedent from "dedent"; | ||
|
||
let cleanupFunctions: (() => Promise<void>)[] = []; | ||
const registerCleanup = (cleanup: () => Promise<void>) => { | ||
cleanupFunctions.push(cleanup); | ||
}; | ||
afterEach(async () => { | ||
const cleanupFnsCopy = cleanupFunctions.slice().reverse(); | ||
cleanupFunctions = []; | ||
for (const cleanup of cleanupFnsCopy) { | ||
try { | ||
await cleanup(); | ||
} catch (error) { | ||
console.error("Error during cleanup:", error); | ||
} | ||
} | ||
}); | ||
|
||
interface SetupProps { | ||
skipAgentAPIMock?: boolean; | ||
skipCodexMock?: boolean; | ||
moduleVariables?: Record<string, string>; | ||
agentapiMockScript?: string; | ||
} | ||
|
||
const setup = async (props?: SetupProps): Promise<{ id: string }> => { | ||
const projectDir = "/home/coder/project"; | ||
const { id } = await setupUtil({ | ||
moduleDir: import.meta.dir, | ||
moduleVariables: { | ||
install_codex: props?.skipCodexMock ? "true" : "false", | ||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false", | ||
codex_model: "gpt-4-turbo", | ||
...props?.moduleVariables, | ||
}, | ||
registerCleanup, | ||
projectDir, | ||
skipAgentAPIMock: props?.skipAgentAPIMock, | ||
agentapiMockScript: props?.agentapiMockScript, | ||
}); | ||
if (!props?.skipCodexMock) { | ||
await writeExecutable({ | ||
containerId: id, | ||
filePath: "/usr/bin/codex", | ||
content: await loadTestFile(import.meta.dir, "codex-mock.sh"), | ||
}); | ||
} | ||
return { id }; | ||
}; | ||
|
||
setDefaultTimeout(60 * 1000); | ||
|
||
describe("codex", async () => { | ||
beforeAll(async () => { | ||
await runTerraformInit(import.meta.dir); | ||
}); | ||
|
||
test("happy-path", async () => { | ||
const { id } = await setup(); | ||
await execModuleScript(id); | ||
await expectAgentAPIStarted(id); | ||
}); | ||
|
||
test("install-codex-version", async () => { | ||
const version_to_install = "0.10.0"; | ||
const { id } = await setup({ | ||
skipCodexMock: true, | ||
moduleVariables: { | ||
install_codex: "true", | ||
codex_version: version_to_install, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const resp = await execContainer(id, [ | ||
"bash", | ||
"-c", | ||
`cat /home/coder/.codex-module/install.log`, | ||
]); | ||
expect(resp.stdout).toContain(version_to_install); | ||
}); | ||
|
||
test("codex-config-toml", async () => { | ||
const settings = dedent` | ||
[mcp_servers.CustomMCP] | ||
command = "/Users/jkmr/Documents/work/coder/coder_darwin_arm64" | ||
args = ["exp", "mcp", "server", "app-status-slug=codex"] | ||
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" | ||
`.trim(); | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
extra_codex_settings_toml: settings, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); | ||
expect(resp).toContain("[mcp_servers.CustomMCP]"); | ||
expect(resp).toContain("[mcp_servers.Coder]"); | ||
}); | ||
|
||
test("codex-api-key", async () => { | ||
const apiKey = "test-api-key-123"; | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
openai_api_key: apiKey, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
|
||
const resp = await readFileContainer(id, "/home/coder/.codex-module/agentapi-start.log"); | ||
expect(resp).toContain("openai_api_key provided !"); | ||
}); | ||
|
||
test("pre-post-install-scripts", async () => { | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
pre_install_script: "#!/bin/bash\necho 'pre-install-script'", | ||
post_install_script: "#!/bin/bash\necho 'post-install-script'", | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const preInstallLog = await readFileContainer(id, "/home/coder/.codex-module/pre_install.log"); | ||
expect(preInstallLog).toContain("pre-install-script"); | ||
const postInstallLog = await readFileContainer(id, "/home/coder/.codex-module/post_install.log"); | ||
expect(postInstallLog).toContain("post-install-script"); | ||
}); | ||
|
||
test("folder-variable", async () => { | ||
const folder = "/tmp/codex-test-folder"; | ||
const { id } = await setup({ | ||
skipCodexMock: false, | ||
moduleVariables: { | ||
folder, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const resp = await readFileContainer(id, "/home/coder/.codex-module/install.log"); | ||
expect(resp).toContain(folder); | ||
}); | ||
|
||
test("additional-extensions", async () => { | ||
const additional = dedent` | ||
[mcp_servers.CustomMCP] | ||
command = "/Users/jkmr/Documents/work/coder/coder_darwin_arm64" | ||
args = ["exp", "mcp", "server", "app-status-slug=codex"] | ||
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" | ||
`.trim(); | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
additional_extensions: additional, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml"); | ||
expect(resp).toContain("[mcp_servers.CustomMCP]"); | ||
expect(resp).toContain("[mcp_servers.Coder]"); | ||
}); | ||
|
||
test("codex-system-prompt", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you test the "skip appending system prompt if already appended" functionality? |
||
const prompt = "This is a system prompt for Codex."; | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
codex_system_prompt: prompt, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const resp = await readFileContainer(id, "/home/coder/AGENTS.md"); | ||
expect(resp).toContain(prompt); | ||
}); | ||
|
||
test("codex-ai-task-prompt", async () => { | ||
const prompt = "This is a system prompt for Codex."; | ||
const { id } = await setup({ | ||
moduleVariables: { | ||
ai_prompt: prompt, | ||
}, | ||
}); | ||
await execModuleScript(id); | ||
const resp = await execContainer(id, [ | ||
"bash", | ||
"-c", | ||
`cat /home/coder/.codex-module/agentapi-start.log`, | ||
]); | ||
expect(resp.stdout).toContain(`Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: ${prompt}`); | ||
}); | ||
|
||
test("start-without-prompt", async () => { | ||
const { id } = await setup(); | ||
await execModuleScript(id); | ||
const prompt = await execContainer(id, ["ls", "-l", "/home/coder/AGENTS.md"]); | ||
expect(prompt.exitCode).not.toBe(0); | ||
expect(prompt.stderr).toContain("No such file or directory"); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.