Compare commits

..

1 Commits

35 changed files with 1462 additions and 2388 deletions

View File

@ -37,7 +37,7 @@ jobs:
all:
- '**'
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@2b778f292c2ddf8ac261683d0d5d8a18da1512f6 # v2.33.3
uses: coder/coder/.github/actions/setup-tf@34584e909bbe6f501fb2cbdc994325b4d3f9e2ef # v2.32.0
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
@ -87,13 +87,13 @@ jobs:
bun-version: latest
# Need Terraform for its formatter
- name: Install Terraform
uses: coder/coder/.github/actions/setup-tf@2b778f292c2ddf8ac261683d0d5d8a18da1512f6 # v2.33.3
uses: coder/coder/.github/actions/setup-tf@34584e909bbe6f501fb2cbdc994325b4d3f9e2ef # v2.32.0
- name: Install dependencies
run: bun install
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@aca895bf05aec0cb7dffa6f94495e923224d9f17 # v1.46.2
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
with:
config: .github/typos.toml
validate-readme-files:

View File

@ -31,7 +31,7 @@ jobs:
bun-version: latest
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@2b778f292c2ddf8ac261683d0d5d8a18da1512f6 # v2.33.3
uses: coder/coder/.github/actions/setup-tf@34584e909bbe6f501fb2cbdc994325b4d3f9e2ef # v2.32.0
- name: Install dependencies
run: bun install

View File

@ -27,7 +27,7 @@ jobs:
persist-credentials: false
- name: Run zizmor (blocking, HIGH only)
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
with:
advanced-security: false
annotations: true
@ -49,7 +49,7 @@ jobs:
persist-credentials: false
- name: Run zizmor (SARIF)
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
with:
inputs: |
.github/workflows

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="7" height="7" x="14" y="3" rx="1"/>
<path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/>
</svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="7" x="3" y="3" rx="1"/>
<rect width="9" height="7" x="3" y="14" rx="1"/>
<rect width="5" height="7" x="16" y="14" rx="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 336 B

View File

@ -1,107 +1,149 @@
---
display_name: Codex CLI
icon: ../../../../.icons/openai.svg
description: Install and configure the Codex CLI in your workspace.
description: Run Codex CLI in your workspace with AgentAPI integration
verified: true
tags: [agent, codex, ai, openai, ai-gateway]
tags: [agent, codex, ai, openai, tasks, aibridge]
---
# Codex CLI
Install and configure the [Codex CLI](https://github.com/openai/codex) in your workspace.
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 = "5.0.0"
agent_id = coder_agent.main.id
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
}
```
> [!WARNING]
> If upgrading from v4.x.x of this module: v5 is a major refactor that drops support for [Coder Tasks](https://coder.com/docs/ai-coder/tasks) and [Boundary](https://coder.com/docs/ai-coder/agent-firewall). v5 also assumes npm is pre-installed; it no longer bootstraps Node.js. Keep using v4.x.x if you depend on them. See the [PR description](https://github.com/coder/registry/pull/879) for a full migration guide.
## Prerequisites
- OpenAI API key for Codex access
## Examples
### Standalone mode with a launcher app
```tf
locals {
codex_workdir = "/home/coder/project"
}
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
agent_id = coder_agent.main.id
workdir = local.codex_workdir
openai_api_key = var.openai_api_key
}
resource "coder_app" "codex" {
agent_id = coder_agent.main.id
slug = "codex"
display_name = "Codex"
icon = "/icon/openai.svg"
open_in = "slim-window"
command = <<-EOT
#!/bin/bash
set -e
cd "${local.codex_workdir}"
codex
EOT
}
```
> [!NOTE]
> The `coder_app` command re-executes on every pane reconnect. This works for interactive `codex` (which stays alive), but one-shot commands like `codex exec` will re-run each time. For one-shot prompts, use a `coder_script` (runs once at startup) and a `coder_app` that attaches to the existing session (e.g. via tmux/screen).
### Usage with AI Gateway
[AI Gateway](https://coder.com/docs/ai-coder/ai-gateway) is a Premium Coder feature that provides centralized LLM proxy management. Requires Coder >= 2.30.0.
### Run standalone
```tf
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
agent_id = coder_agent.main.id
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
enable_ai_gateway = true
report_tasks = false
}
```
When `enable_ai_gateway = true`, the module configures Codex to use the `aigateway` model provider in `config.toml` with the workspace owner's session token for authentication.
### Usage with AI Bridge
> [!CAUTION]
> `enable_ai_gateway = true` is mutually exclusive with `openai_api_key`. Setting both fails at plan time.
[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. Requires Coder version 2.30+
For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below.
#### Standalone usage with AI Bridge
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.3.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
enable_aibridge = true
}
```
When `enable_aibridge = true`, the module:
- Configures Codex to use the aibridge model_provider with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token
```toml
model_provider = "aibridge"
[model_providers.aibridge]
name = "AI Bridge"
base_url = "https://example.coder.com/api/v2/aibridge/openai/v1"
env_key = "CODER_AIBRIDGE_SESSION_TOKEN"
wire_api = "responses"
```
This allows Codex to route API requests through Coder's AI Bridge instead of directly to OpenAI's API.
Template build will fail if `openai_api_key` is provided alongside `enable_aibridge = true`.
### Usage with Tasks
This example shows how to configure Codex with Coder tasks.
```tf
resource "coder_ai_task" "task" {
count = data.coder_workspace.me.start_count
app_id = module.codex.task_app_id
}
data "coder_task" "me" {}
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_task.me.prompt
workdir = "/home/coder/project"
# Optional: route through AI Bridge (Premium feature)
# enable_aibridge = true
}
```
### Usage with Agent Boundaries
This example shows how to configure the Codex module to run the agent behind a process-level boundary that restricts its network access.
By default, when `enable_boundary = true`, the module uses `coder boundary` subcommand (provided by Coder) without requiring any installation.
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.3.1"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
enable_boundary = true
}
```
> [!NOTE]
> If you provide a custom `base_config_toml`, the module writes it verbatim and does not inject `model_provider = "aigateway"` automatically. Add it to your config yourself:
>
> ```toml
> model_provider = "aigateway"
> ```
> For developers: The module also supports installing boundary from a release version (`use_boundary_directly = true`) or compiling from source (`compile_boundary_from_source = true`). These are escape hatches for development and testing purposes.
### Advanced Configuration
This example shows additional configuration options for custom models, MCP servers, and base configuration.
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
agent_id = coder_agent.main.id
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
openai_api_key = var.openai_api_key
codex_version = "0.128.0"
codex_version = "0.1.0" # Pin to a specific version
codex_model = "gpt-4o" # Custom model
# Override default configuration
base_config_toml = <<-EOT
sandbox_mode = "danger-full-access"
approval_policy = "never"
preferred_auth_method = "apikey"
EOT
mcp = <<-EOT
# Add extra MCP servers
additional_mcp_servers = <<-EOT
[mcp_servers.GitHub]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
@ -110,49 +152,61 @@ module "codex" {
}
```
### Serialize a downstream `coder_script` after the install pipeline
> [!WARNING]
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified workdir. 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.
The module exposes the `scripts` output: an ordered list of `coder exp sync` names for the scripts this module creates (pre_install, install, post_install). Scripts that were not configured are absent.
## How it Works
- **Install**: The module installs Codex CLI and sets up the environment
- **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
- **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided)
- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions.
## State Persistence
AgentAPI can save and restore its conversation state to disk across workspace restarts. This complements `continue` (which resumes the Codex CLI session) by also preserving the AgentAPI-level context. Enabled by default, requires agentapi >= v0.12.0 (older versions skip it with a warning).
To disable:
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
resource "coder_script" "post_codex" {
agent_id = coder_agent.main.id
display_name = "Run after Codex install"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -euo pipefail
trap 'coder exp sync complete post-codex' EXIT
coder exp sync want post-codex ${join(" ", module.codex.scripts)}
coder exp sync start post-codex
codex --version
EOT
# ... other config
enable_state_persistence = false
}
```
## Configuration
When no custom `base_config_toml` is provided, the module uses a minimal default with `preferred_auth_method = "apikey"`. For advanced options, see [Codex config docs](https://developers.openai.com/codex/config-advanced).
### Default Configuration
When no custom `base_config_toml` is provided, the module uses these secure defaults:
```toml
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
[sandbox_workspace_write]
network_access = true
```
> [!NOTE]
> 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
Check the log files in `~/.coder-modules/coder-labs/codex/logs/` for detailed information.
- Check installation and startup logs in `~/.codex-module/`
- Ensure your OpenAI API key has access to the specified model
```bash
cat ~/.coder-modules/coder-labs/codex/logs/install.log
cat ~/.coder-modules/coder-labs/codex/logs/pre_install.log
cat ~/.coder-modules/coder-labs/codex/logs/post_install.log
```
> [!IMPORTANT]
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
> The module automatically configures Codex with your API key and model preferences.
> workdir is a required variable for the module to function correctly.
## References
- [Codex CLI Documentation](https://github.com/openai/codex)
- [AI Gateway](https://coder.com/docs/ai-coder/ai-gateway)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
- [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge)

View File

@ -6,67 +6,15 @@ import {
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
execContainer,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
TerraformState,
} from "~test";
import {
extractCoderEnvVars,
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
import path from "path";
interface ModuleScripts {
pre_install?: string;
install: string;
post_install?: string;
}
const SCRIPT_SUFFIXES = [
"Pre-Install Script",
"Install Script",
"Post-Install Script",
] as const;
const collectScripts = (state: TerraformState): ModuleScripts => {
const byDisplayName: Record<string, string> = {};
for (const resource of state.resources) {
if (resource.type !== "coder_script") continue;
for (const instance of resource.instances) {
const attrs = instance.attributes as Record<string, unknown>;
const displayName = attrs.display_name as string | undefined;
const script = attrs.script as string | undefined;
if (displayName && script) {
byDisplayName[displayName] = script;
}
}
}
const scripts: Partial<ModuleScripts> = {};
for (const suffix of SCRIPT_SUFFIXES) {
const key = `Codex: ${suffix}`;
if (!(key in byDisplayName)) continue;
switch (suffix) {
case "Pre-Install Script":
scripts.pre_install = byDisplayName[key];
break;
case "Install Script":
scripts.install = byDisplayName[key];
break;
case "Post-Install Script":
scripts.post_install = byDisplayName[key];
break;
}
}
if (!scripts.install) {
throw new Error("install script not found in terraform state");
}
return scripts as ModuleScripts;
};
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
@ -85,90 +33,36 @@ afterEach(async () => {
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipCodexMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (
props?: SetupProps,
): Promise<{
id: string;
coderEnvVars: Record<string, string>;
scripts: ModuleScripts;
}> => {
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const moduleDir = path.resolve(import.meta.dir);
const state = await runTerraformApply(moduleDir, {
agent_id: "foo",
workdir: projectDir,
install_codex: "false",
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",
workdir: "/home/coder",
...props?.moduleVariables,
});
const scripts = collectScripts(state);
const coderEnvVars = extractCoderEnvVars(state);
const id = await runContainer("codercom/enterprise-node:latest");
registerCleanup(async () => {
if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") {
console.log(`Not removing container ${id} in debug mode`);
return;
}
await removeContainer(id);
});
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: "#!/bin/bash\nexit 0\n",
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipCodexMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/codex",
content: await Bun.file(
path.join(moduleDir, "testdata", "codex-mock.sh"),
).text(),
content: await loadTestFile(import.meta.dir, "codex-mock.sh"),
});
}
return { id, coderEnvVars, scripts };
};
const runScripts = async (
id: string,
scripts: ModuleScripts,
env?: Record<string, string>,
) => {
const entries = env ? Object.entries(env) : [];
const envArgs =
entries.length > 0
? entries
.map(
([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`,
)
.join(" && ") + " && "
: "";
const ordered: [string, string | undefined][] = [
["pre_install", scripts.pre_install],
["install", scripts.install],
["post_install", scripts.post_install],
];
for (const [name, script] of ordered) {
if (!script) continue;
const target = `/tmp/coder-utils-${name}.sh`;
await writeExecutable({
containerId: id,
filePath: target,
content: script,
});
const resp = await execContainer(id, ["bash", "-c", `${envArgs}${target}`]);
if (resp.exitCode !== 0) {
console.log(`script ${name} failed:`);
console.log(resp.stdout);
console.log(resp.stderr);
throw new Error(`coder-utils ${name} script exited ${resp.exitCode}`);
}
}
return { id };
};
setDefaultTimeout(60 * 1000);
@ -179,269 +73,444 @@ describe("codex", async () => {
});
test("happy-path", async () => {
const { id, scripts } = await setup();
await runScripts(id, scripts);
const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
);
expect(installLog).toContain("Skipping Codex installation");
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-codex-version", async () => {
const version = "0.10.0";
const { id, coderEnvVars, scripts } = await setup({
const version_to_install = "0.10.0";
const { id } = await setup({
skipCodexMock: true,
moduleVariables: {
install_codex: "true",
codex_version: version,
codex_version: version_to_install,
},
});
await runScripts(id, scripts, coderEnvVars);
const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
);
expect(installLog).toContain(version);
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("openai-api-key", async () => {
const apiKey = "test-api-key-123";
const { coderEnvVars } = await setup({
test("check-latest-codex-version-works", async () => {
const { id } = await setup({
skipCodexMock: true,
skipAgentAPIMock: true,
moduleVariables: {
openai_api_key: apiKey,
install_codex: "true",
},
});
expect(coderEnvVars["OPENAI_API_KEY"]).toBe(apiKey);
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("base-config-toml", async () => {
const baseConfig = [
'sandbox_mode = "danger-full-access"',
'approval_policy = "never"',
'preferred_auth_method = "apikey"',
"",
"[custom_section]",
"new_feature = true",
].join("\n");
const { id, scripts } = await setup({
const baseConfig = dedent`
sandbox_mode = "danger-full-access"
approval_policy = "never"
preferred_auth_method = "apikey"
[custom_section]
new_feature = true
`.trim();
const { id } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
},
});
await runScripts(id, scripts);
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
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]");
});
test("additional-mcp-servers", async () => {
const additional = [
"[mcp_servers.GitHub]",
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-github"]',
'type = "stdio"',
'description = "GitHub integration"',
].join("\n");
const { id, scripts } = await setup({
test("codex-api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
mcp: additional,
openai_api_key: apiKey,
},
});
await runScripts(id, scripts);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain("[mcp_servers.GitHub]");
expect(resp).toContain("GitHub integration");
});
await execModuleScript(id);
test("minimal-default-config", async () => {
const { id, scripts } = await setup();
await runScripts(id, scripts);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain('preferred_auth_method = "apikey"');
expect(resp).not.toContain("model_provider");
expect(resp).not.toContain("[model_providers.");
expect(resp).not.toContain("model_reasoning_effort");
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, scripts } = await setup({
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'codex-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'codex-post-install-script'",
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});
await runScripts(id, scripts);
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/pre_install.log",
"/home/coder/.codex-module/pre_install.log",
);
expect(preInstallLog).toContain("codex-pre-install-script");
expect(preInstallLog).toContain("pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/post_install.log",
"/home/coder/.codex-module/post_install.log",
);
expect(postInstallLog).toContain("codex-post-install-script");
expect(postInstallLog).toContain("post-install-script");
});
test("workdir-variable", async () => {
const workdir = "/home/coder/codex-test-folder";
const { id, scripts } = await setup({
const workdir = "/tmp/codex-test-workdir";
const { id } = await setup({
skipCodexMock: false,
moduleVariables: {
workdir,
},
});
await runScripts(id, scripts);
const installLog = await readFileContainer(
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
"/home/coder/.codex-module/install.log",
);
expect(installLog).toContain(workdir);
expect(resp).toContain(workdir);
});
test("codex-with-ai-gateway", async () => {
const { id, coderEnvVars, scripts } = await setup({
test("additional-mcp-servers", async () => {
const additional = dedent`
[mcp_servers.GitHub]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
type = "stdio"
description = "GitHub integration"
[mcp_servers.FileSystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
type = "stdio"
description = "File system access"
`.trim();
const { id } = await setup({
moduleVariables: {
enable_ai_gateway: "true",
additional_mcp_servers: additional,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/config.toml");
expect(resp).toContain("[mcp_servers.GitHub]");
expect(resp).toContain("[mcp_servers.FileSystem]");
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 () => {
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/.codex/AGENTS.md");
expect(resp).toContain(prompt);
});
test("codex-system-prompt-skip-append-if-exists", async () => {
const prompt_1 = "This is a system prompt for Codex.";
const prompt_2 = "This is a system prompt for Goose.";
const prompt_3 = dedent`
This is a system prompt for Codex.
This is a system prompt for Gemini.
`.trim();
const pre_install_script = dedent`
#!/bin/bash
mkdir -p /home/coder/.codex
echo -e "${prompt_3}" >> /home/coder/.codex/AGENTS.md
`.trim();
const { id } = await setup({
moduleVariables: {
pre_install_script,
codex_system_prompt: prompt_2,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.codex/AGENTS.md");
expect(resp).toContain(prompt_1);
expect(resp).toContain(prompt_2);
// Re-run with a prompt that already exists, it should not append again
const { id: id_2 } = await setup({
moduleVariables: {
pre_install_script,
codex_system_prompt: prompt_1,
},
});
await execModuleScript(id_2);
const resp_2 = await readFileContainer(
id_2,
"/home/coder/.codex/AGENTS.md",
);
expect(resp_2).toContain(prompt_1);
const count = (resp_2.match(new RegExp(prompt_1, "g")) || []).length;
expect(count).toBe(1);
});
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(prompt);
});
test("start-without-prompt", async () => {
const { id } = await setup({
moduleVariables: {
codex_system_prompt: "", // Explicitly disable system prompt
},
});
await execModuleScript(id);
const prompt = await execContainer(id, [
"ls",
"-l",
"/home/coder/.codex/AGENTS.md",
]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
test("codex-continue-capture-new-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
ai_prompt: "test task",
},
});
const workdir = "/home/coder";
const expectedSessionId = "019a1234-5678-9abc-def0-123456789012";
const sessionsDir = "/home/coder/.codex/sessions";
const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`;
await execContainer(id, ["mkdir", "-p", sessionsDir]);
await execContainer(id, [
"bash",
"-c",
`echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`,
]);
await execModuleScript(id);
await expectAgentAPIStarted(id);
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
const maxAttempts = 30;
let trackingFileContents = "";
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await execContainer(id, [
"bash",
"-c",
`cat ${trackingFile} 2>/dev/null || echo ""`,
]);
if (result.stdout.trim().length > 0) {
trackingFileContents = result.stdout;
break;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`);
const startLog = await readFileContainer(
id,
"/home/coder/.codex-module/agentapi-start.log",
);
expect(startLog).toContain("Capturing new session ID");
expect(startLog).toContain("Session tracked");
expect(startLog).toContain(expectedSessionId);
});
test("codex-continue-resume-existing-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
ai_prompt: "test prompt",
},
});
const workdir = "/home/coder";
const mockSessionId = "019a1234-5678-9abc-def0-123456789012";
const trackingFile = "/home/coder/.codex-module/.codex-task-session";
await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]);
await execContainer(id, [
"bash",
"-c",
`echo "${workdir}|${mockSessionId}" > ${trackingFile}`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.codex-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("Found existing task session");
expect(startLog.stdout).toContain(mockSessionId);
expect(startLog.stdout).toContain("Resuming existing session");
expect(startLog.stdout).toContain(
`Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`,
);
expect(startLog.stdout).not.toContain("test prompt");
});
test("codex-with-aibridge", async () => {
const { id } = await setup({
moduleVariables: {
enable_aibridge: "true",
model_reasoning_effort: "none",
},
});
await runScripts(id, scripts, coderEnvVars);
await execModuleScript(id);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).toContain('model_provider = "aigateway"');
expect(configToml).toContain('model_reasoning_effort = "none"');
expect(configToml).toContain("[model_providers.aigateway]");
expect(configToml).toContain('model_provider = "aibridge"');
});
test("model-reasoning-effort-standalone", async () => {
const { id, scripts } = await setup({
test("boundary-enabled", async () => {
const { id } = await setup({
moduleVariables: {
model_reasoning_effort: "high",
enable_boundary: "true",
boundary_config_path: "/tmp/test-boundary.yaml",
},
});
await runScripts(id, scripts);
const configToml = await readFileContainer(
// Write boundary config
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/test-boundary.yaml <<'EOF'
jail_type: landjail
proxy_port: 8087
log_level: warn
allowlist:
- "domain=api.openai.com"
EOF`,
]);
// Add mock coder binary for boundary setup
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: `#!/bin/bash
if [ "$1" = "boundary" ]; then
if [ "$2" = "--help" ]; then
echo "boundary help"
exit 0
fi
shift; shift; exec "$@"
fi
echo "mock coder"`,
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
// Verify boundary wrapper was used in start script
const startLog = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
"/home/coder/.codex-module/agentapi-start.log",
);
expect(configToml).toContain('model_reasoning_effort = "high"');
expect(configToml).not.toContain("model_provider");
});
test("workdir-trusted-project", async () => {
const workdir = "/home/coder/trusted-project";
const { id, scripts } = await setup({
moduleVariables: {
workdir,
},
});
await runScripts(id, scripts);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).toContain(`[projects."${workdir}"]`);
expect(configToml).toContain('trust_level = "trusted"');
});
test("no-workdir-no-project-section", async () => {
const { id, scripts } = await setup({
moduleVariables: {
workdir: "",
},
});
await runScripts(id, scripts);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).not.toContain("[projects.");
});
test("ai-gateway-with-custom-base-config", async () => {
const baseConfig = [
'sandbox_mode = "danger-full-access"',
'model_provider = "aigateway"',
].join("\n");
const { id, coderEnvVars, scripts } = await setup({
moduleVariables: {
enable_ai_gateway: "true",
base_config_toml: baseConfig,
},
});
await runScripts(id, scripts, coderEnvVars);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).toContain('model_provider = "aigateway"');
expect(configToml).toContain("[model_providers.aigateway]");
});
test("ai-gateway-custom-config-no-duplicate-provider", async () => {
const baseConfig = [
'model_provider = "aigateway"',
"",
"[model_providers.aigateway]",
'name = "Custom AI Bridge"',
'base_url = "https://custom.example.com"',
'env_key = "CODER_AIBRIDGE_SESSION_TOKEN"',
'wire_api = "responses"',
].join("\n");
const { id, coderEnvVars, scripts } = await setup({
moduleVariables: {
enable_ai_gateway: "true",
base_config_toml: baseConfig,
},
});
await runScripts(id, scripts, coderEnvVars);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
const matches = configToml.match(/\[model_providers\.aigateway\]/g) || [];
expect(matches.length).toBe(1);
expect(configToml).toContain("Custom AI Bridge");
});
test("install-codex-latest", async () => {
const { id, coderEnvVars, scripts } = await setup({
skipCodexMock: true,
moduleVariables: {
install_codex: "true",
},
});
await runScripts(id, scripts, coderEnvVars);
const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
);
expect(installLog).toContain("Installed Codex CLI");
});
test("custom-config-drops-reasoning-effort", async () => {
const baseConfig = [
'sandbox_mode = "danger-full-access"',
'preferred_auth_method = "apikey"',
].join("\n");
const { id, scripts } = await setup({
moduleVariables: {
base_config_toml: baseConfig,
model_reasoning_effort: "high",
},
});
await runScripts(id, scripts);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).toContain('sandbox_mode = "danger-full-access"');
expect(configToml).not.toContain("model_reasoning_effort");
expect(startLog).toContain("boundary");
});
});

View File

@ -18,6 +18,18 @@ data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
@ -26,8 +38,106 @@ variable "icon" {
variable "workdir" {
type = string
description = "Optional project directory. When set, the module pre-creates it if missing and adds it as a trusted project in Codex config.toml."
default = null
description = "The folder to run Codex in."
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Codex"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Codex"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Codex CLI"
}
variable "enable_aibridge" {
type = bool
description = "Use AI Bridge for Codex. https://coder.com/docs/ai-coder/ai-bridge"
default = false
validation {
condition = !(var.enable_aibridge && length(var.openai_api_key) > 0)
error_message = "openai_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials."
}
}
variable "model_reasoning_effort" {
type = string
description = "The reasoning effort for the model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort"
default = ""
validation {
condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort)
error_message = "model_reasoning_effort must be one of: none, low, medium, high."
}
}
variable "install_codex" {
type = bool
description = "Whether to install Codex."
default = true
}
variable "codex_version" {
type = string
description = "The version of Codex to install."
default = "" # empty string means the latest available version
}
variable "base_config_toml" {
type = string
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 = ""
}
variable "additional_mcp_servers" {
type = string
description = "Additional MCP servers configuration in TOML format. These will be merged with the required Coder MCP server in the [mcp_servers] section."
default = ""
}
variable "openai_api_key" {
type = string
description = "OpenAI API key for Codex CLI"
default = ""
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.12.1"
}
variable "codex_model" {
type = string
description = "The model for Codex to use. Defaults to gpt-5.3-codex."
default = "gpt-5.4"
}
variable "pre_install_script" {
@ -42,127 +152,158 @@ variable "post_install_script" {
default = null
}
variable "install_codex" {
variable "ai_prompt" {
type = string
description = "Initial task prompt for Codex CLI when launched via Tasks"
default = ""
}
variable "continue" {
type = bool
description = "Whether to install Codex."
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
default = true
}
variable "codex_version" {
variable "enable_state_persistence" {
type = bool
description = "Enable AgentAPI conversation state persistence across restarts."
default = true
}
variable "codex_system_prompt" {
type = string
description = "The version of Codex to install."
description = "System instructions written to AGENTS.md in the ~/.codex directory"
default = "You are a helpful coding assistant. Start every response with `Codex says:`"
}
variable "enable_boundary" {
type = bool
description = "Enable coder boundary for network filtering."
default = false
}
variable "boundary_config_path" {
type = string
description = "Path to boundary config.yaml inside the workspace. If provided, exposed as BOUNDARY_CONFIG env var."
default = ""
}
variable "boundary_version" {
type = string
description = "Boundary version. When use_boundary_directly is true, a release version should be provided or 'latest' for the latest release."
default = "latest"
}
variable "openai_api_key" {
type = string
description = "OpenAI API key for Codex CLI."
sensitive = true
default = ""
}
variable "base_config_toml" {
type = string
description = <<-EOT
Complete base TOML configuration for Codex (without mcp_servers section).
When empty, the module generates a minimal default:
preferred_auth_method = "apikey"
# model_provider = "aigateway" (sets the default profile, when enable_ai_gateway = true)
# model_reasoning_effort = "<value>" (sets the reasoning effort, when model_reasoning_effort is set)
[projects."<workdir>"] (when workdir is set)
trust_level = "trusted"
When non-empty, the value is written verbatim as the base of config.toml;
mcp and AI Gateway sections are still appended after it.
Note: model_reasoning_effort and workdir trust are only applied in the
default config. Include them in your custom config if needed.
EOT
default = ""
}
variable "mcp" {
type = string
description = "MCP server configurations in TOML format. When set, servers are appended to the Codex config.toml."
default = ""
}
variable "model_reasoning_effort" {
type = string
description = "The reasoning effort for the model. One of: none, minimal, low, medium, high, xhigh. See https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort"
default = ""
validation {
condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort)
error_message = "model_reasoning_effort must be one of: none, minimal, low, medium, high, xhigh."
}
}
variable "enable_ai_gateway" {
variable "compile_boundary_from_source" {
type = bool
description = "Use AI Gateway for Codex. https://coder.com/docs/ai-coder/ai-gateway"
description = "Whether to compile boundary from source instead of using the official install script."
default = false
}
validation {
condition = !(var.enable_ai_gateway && length(var.openai_api_key) > 0)
error_message = "openai_api_key cannot be provided when enable_ai_gateway is true. AI Gateway automatically authenticates the client using Coder credentials."
}
variable "use_boundary_directly" {
type = bool
description = "Whether to use boundary binary directly instead of coder boundary subcommand."
default = false
}
resource "coder_env" "openai_api_key" {
count = var.openai_api_key != "" ? 1 : 0
agent_id = var.agent_id
name = "OPENAI_API_KEY"
value = var.openai_api_key
}
# Authenticates the client against Coder's AI Gateway using the workspace
# owner's session token. Referenced by config.toml model_providers.aigateway.
resource "coder_env" "ai_gateway_session_token" {
count = var.enable_ai_gateway ? 1 : 0
resource "coder_env" "coder_aibridge_session_token" {
count = var.enable_aibridge ? 1 : 0
agent_id = var.agent_id
name = "CODER_AIBRIDGE_SESSION_TOKEN"
value = data.coder_workspace_owner.me.session_token
}
locals {
workdir = var.workdir != null ? trimsuffix(var.workdir, "/") : ""
workdir = trimsuffix(var.workdir, "/")
app_slug = "codex"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".codex-module"
latest_codex_model = "gpt-5.4"
aibridge_config = <<-EOF
[model_providers.aigateway]
name = "AI Gateway"
[model_providers.aibridge]
name = "AI Bridge"
base_url = "${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1"
env_key = "CODER_AIBRIDGE_SESSION_TOKEN"
wire_api = "responses"
EOF
install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
ARG_INSTALL = tostring(var.install_codex)
ARG_CODEX_VERSION = var.codex_version != "" ? base64encode(var.codex_version) : ""
ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : ""
ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : ""
ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : ""
ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway)
ARG_AIBRIDGE_CONFIG = var.enable_ai_gateway ? base64encode(local.aibridge_config) : ""
ARG_MODEL_REASONING_EFFORT = var.model_reasoning_effort
ARG_OPENAI_API_KEY = var.openai_api_key != "" ? base64encode(var.openai_api_key) : ""
})
module_dir_name = ".coder-modules/coder-labs/codex"
}
module "coder_utils" {
source = "registry.coder.com/coder/coder-utils/coder"
version = "0.0.1"
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.3.0"
agent_id = var.agent_id
module_directory = "$HOME/${local.module_dir_name}"
display_name_prefix = "Codex"
icon = var.icon
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_subdomain = var.subdomain
agentapi_version = var.agentapi_version
enable_state_persistence = var.enable_state_persistence
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
install_script = local.install_script
enable_boundary = var.enable_boundary
boundary_config_path = var.boundary_config_path
boundary_version = var.boundary_version
compile_boundary_from_source = var.compile_boundary_from_source
use_boundary_directly = var.use_boundary_directly
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_CODEX_MODEL='${var.codex_model}' \
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_CONTINUE='${var.continue}' \
ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_CODEX_MODEL='${var.codex_model}' \
ARG_LATEST_CODEX_MODEL='${local.latest_codex_model}' \
ARG_INSTALL='${var.install_codex}' \
ARG_CODEX_VERSION='${var.codex_version}' \
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \
ARG_AIBRIDGE_CONFIG='${base64encode(var.enable_aibridge ? local.aibridge_config : "")}' \
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_MODEL_REASONING_EFFORT='${var.model_reasoning_effort}' \
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
/tmp/install.sh
EOT
}
output "scripts" {
description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list."
value = module.coder_utils.scripts
output "task_app_id" {
value = module.agentapi.task_app_id
}

View File

@ -4,15 +4,41 @@ run "test_codex_basic" {
variables {
agent_id = "test-agent"
workdir = "/home/coder"
openai_api_key = "test-key"
}
assert {
condition = var.agent_id == "test-agent"
error_message = "Agent ID should be set correctly"
}
assert {
condition = var.workdir == "/home/coder"
error_message = "Workdir should be set correctly"
}
assert {
condition = var.install_codex == true
error_message = "install_codex should default to true"
}
assert {
condition = var.install_agentapi == true
error_message = "install_agentapi should default to true"
}
assert {
condition = var.report_tasks == true
error_message = "report_tasks should default to true"
}
assert {
condition = var.continue == true
error_message = "continue should default to true"
}
}
run "test_codex_with_api_key" {
run "test_enable_state_persistence_default" {
command = plan
variables {
@ -22,96 +48,126 @@ run "test_codex_with_api_key" {
}
assert {
condition = coder_env.openai_api_key[0].value == "test-key"
condition = var.enable_state_persistence == true
error_message = "enable_state_persistence should default to true"
}
}
run "test_disable_state_persistence" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
openai_api_key = "test-key"
enable_state_persistence = false
}
assert {
condition = var.enable_state_persistence == false
error_message = "enable_state_persistence should be false when explicitly disabled"
}
}
run "test_codex_with_aibridge" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
enable_aibridge = true
}
assert {
condition = var.enable_aibridge == true
error_message = "enable_aibridge should be set to true"
}
}
run "test_aibridge_disabled_with_api_key" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
openai_api_key = "test-key"
enable_aibridge = false
}
assert {
condition = var.enable_aibridge == false
error_message = "enable_aibridge should be false"
}
assert {
condition = coder_env.openai_api_key.value == "test-key"
error_message = "OpenAI API key should be set correctly"
}
}
run "test_codex_custom_options" {
run "test_custom_options" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
icon = "/icon/custom.svg"
codex_version = "0.128.0"
}
assert {
condition = length(output.scripts) > 0
error_message = "scripts output should be non-empty with custom options"
}
}
run "test_ai_gateway_enabled" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
enable_ai_gateway = true
}
override_data {
target = data.coder_workspace_owner.me
values = {
session_token = "mock-session-token"
}
}
assert {
condition = coder_env.ai_gateway_session_token[0].name == "CODER_AIBRIDGE_SESSION_TOKEN"
error_message = "CODER_AIBRIDGE_SESSION_TOKEN should be set"
}
assert {
condition = coder_env.ai_gateway_session_token[0].value == data.coder_workspace_owner.me.session_token
error_message = "Session token should use workspace owner's token"
}
assert {
condition = length(coder_env.openai_api_key) == 0
error_message = "OPENAI_API_KEY should not be created when ai_gateway is enabled"
}
}
run "test_ai_gateway_validation_with_api_key" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
enable_ai_gateway = true
openai_api_key = "test-key"
}
expect_failures = [
var.enable_ai_gateway,
]
}
run "test_ai_gateway_disabled_with_api_key" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
enable_ai_gateway = false
openai_api_key = "test-key-xyz"
order = 5
group = "ai-tools"
icon = "/icon/custom.svg"
web_app_display_name = "Custom Codex"
cli_app = true
cli_app_display_name = "Codex Terminal"
subdomain = true
report_tasks = false
continue = false
codex_model = "gpt-4o"
codex_version = "0.1.0"
agentapi_version = "v0.12.0"
}
assert {
condition = coder_env.openai_api_key[0].value == "test-key-xyz"
error_message = "OPENAI_API_KEY should use the provided API key"
condition = var.order == 5
error_message = "Order should be set to 5"
}
assert {
condition = length(coder_env.ai_gateway_session_token) == 0
error_message = "Session token should not be set when ai_gateway is disabled"
condition = var.group == "ai-tools"
error_message = "Group should be set to 'ai-tools'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon should be set to custom icon"
}
assert {
condition = var.cli_app == true
error_message = "cli_app should be enabled"
}
assert {
condition = var.subdomain == true
error_message = "subdomain should be enabled"
}
assert {
condition = var.report_tasks == false
error_message = "report_tasks should be disabled"
}
assert {
condition = var.continue == false
error_message = "continue should be disabled"
}
assert {
condition = var.codex_model == "gpt-4o"
error_message = "codex_model should be set to 'gpt-4o'"
}
}
run "test_no_api_key_no_env" {
run "test_no_api_key_no_aibridge" {
command = plan
variables {
@ -120,66 +176,12 @@ run "test_no_api_key_no_env" {
}
assert {
condition = length(coder_env.openai_api_key) == 0
error_message = "OPENAI_API_KEY should not be created when no API key is provided"
}
}
run "test_codex_with_scripts" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
condition = var.openai_api_key == ""
error_message = "openai_api_key should be empty when not provided"
}
assert {
condition = length(output.scripts) == 3
error_message = "scripts output should have 3 entries when pre/post are configured"
}
}
run "test_script_outputs_install_only" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = length(output.scripts) == 1 && output.scripts[0] == "coder-labs-codex-install_script"
error_message = "scripts output should list only the install script when pre/post are not configured"
}
}
run "test_script_outputs_with_pre_and_post" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
pre_install_script = "echo pre"
post_install_script = "echo post"
}
assert {
condition = output.scripts == ["coder-labs-codex-pre_install_script", "coder-labs-codex-install_script", "coder-labs-codex-post_install_script"]
error_message = "scripts output should list pre_install, install, post_install in run order"
}
}
run "test_workdir_optional" {
command = plan
variables {
agent_id = "test-agent"
}
assert {
condition = length(output.scripts) == 1
error_message = "scripts output should have install script even without workdir"
condition = var.enable_aibridge == false
error_message = "enable_aibridge should default to false"
}
}

View File

@ -0,0 +1,228 @@
#!/bin/bash
source "$HOME"/.bashrc
BOLD='\033[0;1m'
command_exists() {
command -v "$1" > /dev/null 2>&1
}
set -o errexit
set -o pipefail
set -o nounset
ARG_BASE_CONFIG_TOML=$(echo -n "$ARG_BASE_CONFIG_TOML" | 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_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false}
ARG_AIBRIDGE_CONFIG=$(echo -n "$ARG_AIBRIDGE_CONFIG" | base64 -d)
echo "=== Codex Module Configuration ==="
printf "Install Codex: %s\n" "$ARG_INSTALL"
printf "Codex Version: %s\n" "$ARG_CODEX_VERSION"
printf "App Slug: %s\n" "$ARG_CODER_MCP_APP_STATUS_SLUG"
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
printf "Latest Codex Model: %s\n" "${ARG_LATEST_CODEX_MODEL}"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
printf "Enable Coder AI Bridge: %s\n" "$ARG_ENABLE_AIBRIDGE"
echo "======================================"
set +o nounset
function install_node() {
if ! command_exists npm; then
printf "npm not found, checking for Node.js installation...\n"
if ! command_exists node; then
printf "Node.js not found, installing Node.js via NVM...\n"
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
nvm install --lts
nvm use --lts
nvm alias default node
printf "Node.js installed: %s\n" "$(node --version)"
printf "npm installed: %s\n" "$(npm --version)"
else
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
exit 1
fi
fi
}
function install_codex() {
if [ "${ARG_INSTALL}" = "true" ]; then
install_node
if ! command_exists nvm; then
printf "which node: %s\n" "$(which node)"
printf "which npm: %s\n" "$(which npm)"
mkdir -p "$HOME"/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
fi
fi
printf "%s Installing Codex CLI\n" "${BOLD}"
if [ -n "$ARG_CODEX_VERSION" ]; then
npm install -g "@openai/codex@$ARG_CODEX_VERSION"
else
npm install -g "@openai/codex"
fi
printf "%s Successfully installed Codex CLI. Version: %s\n" "${BOLD}" "$(codex --version)"
fi
}
write_minimal_default_config() {
local config_path="$1"
ARG_OPTIONAL_TOP_LEVEL_CONFIG=""
if [[ "${ARG_ENABLE_AIBRIDGE}" = "true" ]]; then
ARG_OPTIONAL_TOP_LEVEL_CONFIG='model_provider = "aibridge"'
fi
if [[ "${ARG_MODEL_REASONING_EFFORT}" != "" ]]; then
ARG_OPTIONAL_TOP_LEVEL_CONFIG+=$'\n'"model_reasoning_effort = \"${ARG_MODEL_REASONING_EFFORT}\""
fi
cat << EOF > "$config_path"
# Minimal Default Codex Configuration
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
${ARG_OPTIONAL_TOP_LEVEL_CONFIG}
[sandbox_workspace_write]
network_access = true
[notice.model_migrations]
"${ARG_CODEX_MODEL}" = "${ARG_LATEST_CODEX_MODEL}"
[projects."${ARG_CODEX_START_DIRECTORY}"]
trust_level = "trusted"
EOF
}
append_mcp_servers_section() {
local config_path="$1"
if [ "${ARG_REPORT_TASKS}" == "false" ]; then
ARG_CODER_MCP_APP_STATUS_SLUG=""
CODER_MCP_AI_AGENTAPI_URL=""
else
CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
fi
cat << EOF >> "$config_path"
# MCP Servers Configuration
[mcp_servers.Coder]
command = "coder"
args = ["exp", "mcp", "server"]
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "${CODER_MCP_AI_AGENTAPI_URL}" , "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}", "CODER_MCP_ALLOWED_TOOLS" = "coder_report_task" }
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
type = "stdio"
EOF
if [ -n "$ARG_ADDITIONAL_MCP_SERVERS" ]; then
printf "Adding additional MCP servers\n"
echo "$ARG_ADDITIONAL_MCP_SERVERS" >> "$config_path"
fi
}
append_aibridge_config_section() {
local config_path="$1"
if [ -n "$ARG_AIBRIDGE_CONFIG" ]; then
printf "Adding AI Bridge configuration\n"
echo -e "\n# AI Bridge Configuration\n$ARG_AIBRIDGE_CONFIG" >> "$config_path"
fi
}
function populate_config_toml() {
CONFIG_PATH="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$CONFIG_PATH")"
if [ -n "$ARG_BASE_CONFIG_TOML" ]; then
printf "Using provided base configuration\n"
echo "$ARG_BASE_CONFIG_TOML" > "$CONFIG_PATH"
else
printf "Using minimal default configuration\n"
write_minimal_default_config "$CONFIG_PATH"
fi
append_mcp_servers_section "$CONFIG_PATH"
if [ "$ARG_ENABLE_AIBRIDGE" = "true" ]; then
printf "AI Bridge is enabled\n"
append_aibridge_config_section "$CONFIG_PATH"
fi
}
function add_instruction_prompt_if_exists() {
if [ -n "${ARG_CODEX_INSTRUCTION_PROMPT:-}" ]; then
AGENTS_PATH="$HOME/.codex/AGENTS.md"
printf "Creating AGENTS.md in .codex directory: %s\\n" "${AGENTS_PATH}"
mkdir -p "$HOME/.codex"
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
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}" || {
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
fi
else
printf "AGENTS.md instruction prompt is not set.\n"
fi
}
function add_auth_json() {
AUTH_JSON_PATH="$HOME/.codex/auth.json"
mkdir -p "$(dirname "$AUTH_JSON_PATH")"
AUTH_JSON=$(
cat << EOF
{
"OPENAI_API_KEY": "${ARG_OPENAI_API_KEY}"
}
EOF
)
echo "$AUTH_JSON" > "$AUTH_JSON_PATH"
}
install_codex
codex --version
populate_config_toml
add_instruction_prompt_if_exists
if [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then
add_auth_json
fi

View File

@ -1,195 +0,0 @@
#!/bin/bash
set -euo pipefail
BOLD='\033[0;1m'
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_INSTALL='${ARG_INSTALL}'
ARG_CODEX_VERSION=$(echo -n '${ARG_CODEX_VERSION}' | base64 -d)
ARG_WORKDIR=$(echo -n '${ARG_WORKDIR}' | base64 -d)
ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d)
ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d)
ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}'
ARG_AIBRIDGE_CONFIG=$(echo -n '${ARG_AIBRIDGE_CONFIG}' | base64 -d)
ARG_MODEL_REASONING_EFFORT='${ARG_MODEL_REASONING_EFFORT}'
ARG_OPENAI_API_KEY=$(echo -n '${ARG_OPENAI_API_KEY}' | base64 -d)
echo "--------------------------------"
printf "codex_version: %s\n" "$${ARG_CODEX_VERSION}"
printf "workdir: %s\n" "$${ARG_WORKDIR}"
printf "enable_ai_gateway: %s\n" "$${ARG_ENABLE_AI_GATEWAY}"
printf "install_codex: %s\n" "$${ARG_INSTALL}"
printf "model_reasoning_effort: %s\n" "$${ARG_MODEL_REASONING_EFFORT}"
echo "--------------------------------"
function add_path_to_shell_profiles() {
local path_dir="$1"
for profile in "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.zshrc"; do
if [ -f "$${profile}" ]; then
if ! grep -q "$${path_dir}" "$${profile}" 2> /dev/null; then
echo "export PATH=\"\$PATH:$${path_dir}\"" >> "$${profile}"
echo "Added $${path_dir} to $${profile}"
fi
fi
done
local fish_config="$HOME/.config/fish/config.fish"
if [ -f "$${fish_config}" ]; then
if ! grep -q "$${path_dir}" "$${fish_config}" 2> /dev/null; then
echo "fish_add_path $${path_dir}" >> "$${fish_config}"
echo "Added $${path_dir} to $${fish_config}"
fi
fi
}
function ensure_codex_in_path() {
local CODEX_BIN=""
if command -v codex > /dev/null 2>&1; then
CODEX_BIN=$(command -v codex)
elif [ -x "$HOME/.npm-global/bin/codex" ]; then
CODEX_BIN="$HOME/.npm-global/bin/codex"
fi
if [ -z "$${CODEX_BIN}" ] || [ ! -x "$${CODEX_BIN}" ]; then
echo "Warning: Could not find codex binary after install"
return
fi
local CODEX_DIR
CODEX_DIR=$(dirname "$${CODEX_BIN}")
if [ -n "$${CODER_SCRIPT_BIN_DIR:-}" ] && [ ! -e "$${CODER_SCRIPT_BIN_DIR}/codex" ]; then
ln -s "$${CODEX_BIN}" "$${CODER_SCRIPT_BIN_DIR}/codex"
echo "Created symlink: $${CODER_SCRIPT_BIN_DIR}/codex -> $${CODEX_BIN}"
fi
add_path_to_shell_profiles "$${CODEX_DIR}"
}
function install_codex() {
if [ "$${ARG_INSTALL}" != "true" ]; then
echo "Skipping Codex installation as per configuration."
ensure_codex_in_path
return
fi
if [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
fi
# Detect a package manager for global installs.
if command_exists npm; then
PKG_INSTALL="npm install -g"
if ! command_exists nvm; then
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
fi
elif command_exists pnpm; then
PKG_INSTALL="pnpm add -g"
elif command_exists bun; then
PKG_INSTALL="bun add -g"
else
echo "Error: npm, pnpm, or bun is required to install Codex. Install one of them first or set install_codex = false."
exit 1
fi
printf "%s Installing Codex CLI\n" "$${BOLD}"
if [ -n "$${ARG_CODEX_VERSION}" ]; then
$PKG_INSTALL "@openai/codex@$${ARG_CODEX_VERSION}"
else
$PKG_INSTALL "@openai/codex"
fi
printf "%s Installed Codex CLI: %s\n" "$${BOLD}" "$(codex --version)"
ensure_codex_in_path
}
function write_minimal_default_config() {
local config_path="$1"
local optional_config=""
if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ]; then
optional_config='model_provider = "aigateway"'
fi
if [ -n "$${ARG_MODEL_REASONING_EFFORT}" ]; then
optional_config+=$'\n'"model_reasoning_effort = \"$${ARG_MODEL_REASONING_EFFORT}\""
fi
cat << EOF > "$${config_path}"
preferred_auth_method = "apikey"
$${optional_config}
EOF
if [ -n "$${ARG_WORKDIR}" ]; then
cat << EOF >> "$${config_path}"
[projects."$${ARG_WORKDIR}"]
trust_level = "trusted"
EOF
fi
}
function populate_config_toml() {
local config_path="$HOME/.codex/config.toml"
mkdir -p "$(dirname "$${config_path}")"
if [ -n "$${ARG_BASE_CONFIG_TOML}" ]; then
printf "Using provided base configuration\n"
echo "$${ARG_BASE_CONFIG_TOML}" > "$${config_path}"
else
printf "Using minimal default configuration\n"
write_minimal_default_config "$${config_path}"
fi
if [ -n "$${ARG_MCP}" ]; then
printf "Adding MCP servers\n"
echo "$${ARG_MCP}" >> "$${config_path}"
fi
if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then
if ! grep -q '\[model_providers\.aigateway\]' "$${config_path}" 2>/dev/null; then
printf "Adding AI Gateway configuration\n"
echo -e "\n$${ARG_AIBRIDGE_CONFIG}" >> "$${config_path}"
else
printf "AI Gateway provider already defined in config, skipping append\n"
fi
fi
}
function setup_workdir() {
if [ -n "$${ARG_WORKDIR}" ] && [ ! -d "$${ARG_WORKDIR}" ]; then
echo "Creating workdir: $${ARG_WORKDIR}"
mkdir -p "$${ARG_WORKDIR}"
fi
}
function add_auth_json() {
if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] || [ -z "$${ARG_OPENAI_API_KEY}" ]; then
return
fi
local auth_path="$HOME/.codex/auth.json"
mkdir -p "$(dirname "$${auth_path}")"
cat << EOF > "$${auth_path}"
{
"auth_mode": "apikey",
"OPENAI_API_KEY": "$${ARG_OPENAI_API_KEY}"
}
EOF
echo "Seeded auth.json with API key"
}
install_codex
populate_config_toml
setup_workdir
add_auth_json

View File

@ -0,0 +1,229 @@
#!/bin/bash
source "$HOME"/.bashrc
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
fi
printf "Version: %s\n" "$(codex --version)"
set -o nounset
ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d)
ARG_CONTINUE=${ARG_CONTINUE:-true}
ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false}
echo "=== Codex Launch Configuration ==="
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
printf "Continue Sessions: %s\n" "$ARG_CONTINUE"
printf "Enable Coder AI Bridge: %s\n" "$ARG_ENABLE_AIBRIDGE"
echo "======================================"
set +o nounset
SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session"
find_session_for_directory() {
local target_dir="$1"
if [ ! -f "$SESSION_TRACKING_FILE" ]; then
return 1
fi
local session_id
session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1)
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
fi
return 1
}
store_session_mapping() {
local dir="$1"
local session_id="$2"
mkdir -p "$(dirname "$SESSION_TRACKING_FILE")"
if [ -f "$SESSION_TRACKING_FILE" ]; then
grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true
mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE"
fi
echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE"
}
find_recent_session_file() {
local target_dir="$1"
local sessions_dir="$HOME/.codex/sessions"
if [ ! -d "$sessions_dir" ]; then
return 1
fi
local latest_file=""
local latest_time=0
while IFS= read -r session_file; do
local file_time
file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0")
local first_line
first_line=$(head -n 1 "$session_file" 2> /dev/null)
local session_cwd
session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then
latest_file="$session_file"
latest_time="$file_time"
fi
done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null)
if [ -n "$latest_file" ]; then
local first_line
first_line=$(head -n 1 "$latest_file")
local session_id
session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
fi
fi
return 1
}
wait_for_session_file() {
local target_dir="$1"
local max_attempts=20
local attempt=0
while [ $attempt -lt $max_attempts ]; do
local session_id
session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "")
if [ -n "$session_id" ]; then
echo "$session_id"
return 0
fi
sleep 0.5
attempt=$((attempt + 1))
done
return 1
}
validate_codex_installation() {
if command_exists codex; then
printf "Codex is installed\n"
else
printf "Error: Codex is not installed. Please enable install_codex or install it manually\n"
exit 1
fi
}
setup_workdir() {
if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then
printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
cd "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
else
printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}"
mkdir -p "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
cd "${ARG_CODEX_START_DIRECTORY}" || {
printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}"
exit 1
}
fi
}
build_codex_args() {
CODEX_ARGS=()
if [[ -n "${ARG_CODEX_MODEL}" ]]; then
CODEX_ARGS+=("--model" "${ARG_CODEX_MODEL}")
fi
if [ "$ARG_CONTINUE" = "true" ]; then
existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "")
if [ -n "$existing_session" ]; then
printf "Found existing task session for this directory: %s\n" "$existing_session"
printf "Resuming existing session...\n"
CODEX_ARGS+=("resume" "$existing_session")
else
printf "No existing task session found for this directory\n"
printf "Starting new task session...\n"
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
else
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
fi
CODEX_ARGS+=("$PROMPT")
fi
fi
else
printf "Continue disabled, starting fresh session\n"
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using Coder.coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
else
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
fi
CODEX_ARGS+=("$PROMPT")
fi
fi
}
capture_session_id() {
if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then
printf "Capturing new session ID...\n"
new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "")
if [ -n "$new_session" ]; then
store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session"
printf "✓ Session tracked: %s\n" "$new_session"
printf "This session will be automatically resumed on next restart\n"
else
printf "⚠ Could not capture session ID after 10s timeout\n"
fi
fi
}
start_codex() {
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
# AGENTAPI_BOUNDARY_PREFIX is set by the agentapi module's main.sh when
# enable_boundary=true. It points to a wrapper script that runs the command
# through coder boundary, sandboxing only the agent process.
if [ -n "${AGENTAPI_BOUNDARY_PREFIX:-}" ]; then
printf "Starting with coder boundary enabled\n"
agentapi server --type codex --term-width 67 --term-height 1190 -- \
"${AGENTAPI_BOUNDARY_PREFIX}" codex "${CODEX_ARGS[@]}" &
else
agentapi server --type codex --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
fi
capture_session_id
}
validate_codex_installation
setup_workdir
build_codex_args
start_codex

View File

@ -1,9 +1,38 @@
#!/bin/bash
# Handle --version flag
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "codex version v1.0.0"
exit 0
fi
echo "codex invoked with: $*"
exit 0
set -e
SESSION_ID=""
IS_RESUME=false
while [[ $# -gt 0 ]]; do
case $1 in
resume)
IS_RESUME=true
SESSION_ID="$2"
shift 2
;;
*)
shift
;;
esac
done
if [ "$IS_RESUME" = false ]; then
SESSION_ID="019a1234-5678-9abc-def0-123456789012"
echo "Created new session: $SESSION_ID"
else
echo "Resuming session: $SESSION_ID"
fi
while true; do
echo "$(date) - codex-mock (session: $SESSION_ID)"
sleep 15
done

View File

@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "3.0.1"
version = "3.0.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "3.0.1"
version = "3.0.0"
agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
module "gemini" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/gemini/coder"
version = "3.0.1"
version = "3.0.0"
agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key
gemini_model = "gemini-2.5-flash"
@ -105,22 +105,6 @@ module "gemini" {
You are a helpful coding assistant. Always explain your code changes clearly.
YOU MUST REPORT ALL TASKS TO CODER.
EOT
pre_install_script = <<-EOT
#!/bin/bash
set -e
echo "Installing Node.js via NodeSource..."
sudo apt-get update -qq && sudo apt-get install -y curl ca-certificates
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash -
sudo apt-get install -y nodejs
echo "Node version: $(node -v)"
echo "npm version: $(npm -v)"
echo "Node install complete."
EOT
}
```
@ -134,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "3.0.1"
version = "3.0.0"
agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"

View File

@ -148,16 +148,22 @@ locals {
base_extensions = <<-EOT
{
"coder": {
"command": "coder",
"args": [
"exp",
"mcp",
"server"
],
"command": "coder",
"description": "Report ALL tasks and statuses (in progress, done, failed) you are working on.",
"enabled": true,
"env": {
"CODER_MCP_APP_STATUS_SLUG": "${local.app_slug}",
"CODER_MCP_AI_AGENTAPI_URL": "http://localhost:3284"
}
},
"name": "Coder",
"timeout": 3000,
"type": "stdio",
"trust": true
}
}
EOT

View File

@ -17,7 +17,6 @@ echo "--------------------------------"
printf "gemini_config: %s\n" "$ARG_GEMINI_CONFIG"
printf "install: %s\n" "$ARG_INSTALL"
printf "gemini_version: %s\n" "$ARG_GEMINI_VERSION"
printf "BASE_EXTENSIONS: %s\n" "$BASE_EXTENSIONS"
echo "--------------------------------"
set +o nounset
@ -141,25 +140,6 @@ function add_system_prompt_if_exists() {
fi
}
function patch_coder_mcp_command() {
CODER_BIN=$(which coder)
SETTINGS_PATH="$HOME/.gemini/settings.json"
if [ -z "$CODER_BIN" ]; then
printf "Warning: could not find coder binary, MCP command path not patched.\n"
return
fi
printf "Patching coder MCP command path to: %s\n" "$CODER_BIN"
TMP_SETTINGS=$(mktemp)
jq --arg bin "$CODER_BIN" \
'.mcpServers.coder.command = $bin' \
"$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
printf "Patch complete.\n"
}
function configure_mcp() {
export CODER_MCP_APP_STATUS_SLUG="gemini"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
@ -169,5 +149,4 @@ function configure_mcp() {
install_gemini
populate_settings_json
add_system_prompt_if_exists
patch_coder_mcp_command
configure_mcp

View File

@ -1,146 +0,0 @@
---
display_name: Agent Firewall
description: Configures agent-firewall for network isolation in Coder workspaces
icon: ../../../../.icons/coder.svg
verified: true
tags: [agent-firewall, ai, agents, firewall, boundary]
---
# Agent Firewall
Installs [agent-firewall](https://coder.com/docs/ai-coder/agent-firewall) for network isolation in Coder workspaces.
This module:
- Installs agent-firewall (via coder subcommand, direct installation, or compilation from source)
- Creates a wrapper script at `$HOME/.coder-modules/coder/agent-firewall/scripts/agent-firewall-wrapper.sh`
- Writes a [default agent-firewall config](https://github.com/coder/registry/blob/main/registry/coder/modules/agent-firewall/config.yaml.tftpl) to `$HOME/.coder-modules/coder/agent-firewall/config/config.yaml` (customizable)
- Provides the wrapper path, config path, and script names via outputs
- Uses coder-utils and output `scripts` for synchronization. https://registry.coder.com/modules/coder/coder-utils?tab=outputs
```tf
module "agent-firewall" {
source = "registry.coder.com/coder/agent-firewall/coder"
version = "0.0.1"
agent_id = coder_agent.main.id
}
```
## Examples
Use the `agent_firewall_wrapper_path` output to access the wrapper path and `agent_firewall_config_path` to access config path in Terraform and pass it to scripts that should run commands in network isolation.
### With Claude Code
Use agent-firewall alongside the `claude-code` module to run Claude in a
network-isolated environment.
#### As an automated task
```tf
module "agent-firewall" {
source = "registry.coder.com/coder/agent-firewall/coder"
version = "0.0.1"
agent_id = coder_agent.main.id
}
resource "coder_script" "claude_with_agent_firewall" {
agent_id = coder_agent.main.id
display_name = "Claude (Agent Firewall)"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -e
coder exp sync want claude-agent-firewall \
${join(" ", module.agent-firewall.scripts)} \
${join(" ", module.claude-code.scripts)}
coder exp sync start claude-agent-firewall
"${module.agent-firewall.agent_firewall_wrapper_path}" --config="${module.agent-firewall.agent_firewall_config_path}" -- claude -p "Fix issue #840 from coder/coder"
EOT
}
```
#### As a Coder app
```tf
module "agent-firewall" {
source = "registry.coder.com/coder/agent-firewall/coder"
version = "0.0.1"
agent_id = coder_agent.main.id
}
resource "coder_app" "claude_with_agent_firewall" {
agent_id = coder_agent.main.id
display_name = "Claude Code"
slug = "claude-code"
command = <<-EOT
#!/bin/bash
set -e
exec tmux new-session -A -s claude-code \
'"${module.agent-firewall.agent_firewall_wrapper_path}" --config="${module.agent-firewall.agent_firewall_config_path}" -- claude'
EOT
}
```
## Configuration
The module ships with a comprehensive default config based on the
[Coder dogfood allowlist](https://github.com/coder/coder/blob/main/dogfood/coder/boundary-config.yaml). It covers Anthropic services,
OpenAI services, version control, package managers, container registries,
cloud platforms, and common development tools.
The Coder deployment domain is automatically added to the allowlist using
`data.coder_workspace.me.access_url`.
By default the config is written to
`$HOME/.coder-modules/coder/agent-firewall/config/config.yaml`. You can
access the resolved path via the `agent_firewall_config_path` output. Override
it in two ways:
### Inline config
Pass the full YAML content directly:
```tf
module "agent-firewall" {
source = "registry.coder.com/coder/agent-firewall/coder"
version = "0.0.1"
agent_id = coder_agent.main.id
agent_firewall_config = <<-YAML
allowlist:
- domain=your-deployment.coder.com
- domain=api.anthropic.com
- domain=api.openai.com
log_dir: /tmp/agent_firewall_logs
proxy_port: 8087
log_level: warn
YAML
}
```
### External config file
Point to an existing config file in the workspace. The module will not
write any config and the `agent_firewall_config_path` output will point to
your path. The file must exist on disk before agent-firewall starts.
```tf
module "agent-firewall" {
source = "registry.coder.com/coder/agent-firewall/coder"
version = "0.0.1"
agent_id = coder_agent.main.id
agent_firewall_config_path = "/workspace/my-agent-firewall-config.yaml"
}
```
> **Note:** `agent_firewall_config` and `agent_firewall_config_path` are mutually
> exclusive, setting both produces a validation error.
See the [Agent Firewall docs](https://coder.com/docs/ai-coder/agent-firewall)
for the full config reference.
## References
- [Agent Firewall Documentation](https://coder.com/docs/ai-coder/agent-firewall)

View File

@ -1,157 +0,0 @@
# Test for agent-firewall module
run "plan_with_required_vars" {
command = plan
variables {
agent_id = "test-agent-id"
}
# Verify the agent_firewall_wrapper_path output
assert {
condition = output.agent_firewall_wrapper_path == "$HOME/.coder-modules/coder/agent-firewall/scripts/agent-firewall-wrapper.sh"
error_message = "agent_firewall_wrapper_path output should be correct"
}
# Verify agent_firewall_config_path output defaults to the managed path
assert {
condition = output.agent_firewall_config_path == "$HOME/.coder-modules/coder/agent-firewall/config/config.yaml"
error_message = "agent_firewall_config_path output should default to managed config path"
}
# Verify the scripts output contains the install script name
assert {
condition = contains(output.scripts, "coder-agent-firewall-install_script")
error_message = "scripts should contain the install script name"
}
}
run "plan_with_compile_from_source" {
command = plan
variables {
agent_id = "test-agent-id"
compile_agent_firewall_from_source = true
agent_firewall_version = "main"
}
assert {
condition = output.agent_firewall_wrapper_path == "$HOME/.coder-modules/coder/agent-firewall/scripts/agent-firewall-wrapper.sh"
error_message = "agent_firewall_wrapper_path output should be correct"
}
assert {
condition = contains(output.scripts, "coder-agent-firewall-install_script")
error_message = "scripts should contain the install script name"
}
}
run "plan_with_use_directly" {
command = plan
variables {
agent_id = "test-agent-id"
use_agent_firewall_directly = true
agent_firewall_version = "latest"
}
assert {
condition = output.agent_firewall_wrapper_path == "$HOME/.coder-modules/coder/agent-firewall/scripts/agent-firewall-wrapper.sh"
error_message = "agent_firewall_wrapper_path output should be correct"
}
assert {
condition = contains(output.scripts, "coder-agent-firewall-install_script")
error_message = "scripts should contain the install script name"
}
}
run "plan_with_custom_hooks" {
command = plan
variables {
agent_id = "test-agent-id"
pre_install_script = "echo 'Before install'"
post_install_script = "echo 'After install'"
}
assert {
condition = contains(output.scripts, "coder-agent-firewall-install_script")
error_message = "scripts should contain the install script name"
}
# Verify pre and post install script names are set
assert {
condition = contains(output.scripts, "coder-agent-firewall-pre_install_script")
error_message = "scripts should contain the pre_install script name"
}
assert {
condition = contains(output.scripts, "coder-agent-firewall-post_install_script")
error_message = "scripts should contain the post_install script name"
}
}
run "plan_with_custom_module_directory" {
command = plan
variables {
agent_id = "test-agent-id"
module_directory = "$HOME/.coder-modules/custom/agent-firewall"
}
assert {
condition = output.agent_firewall_wrapper_path == "$HOME/.coder-modules/custom/agent-firewall/scripts/agent-firewall-wrapper.sh"
error_message = "agent_firewall_wrapper_path output should use custom module directory"
}
# Config path should also follow the module directory
assert {
condition = output.agent_firewall_config_path == "$HOME/.coder-modules/custom/agent-firewall/config/config.yaml"
error_message = "agent_firewall_config_path output should use custom module directory"
}
}
run "plan_with_inline_config" {
command = plan
variables {
agent_id = "test-agent-id"
agent_firewall_config = "allowlist:\n - domain=example.com\nlog_level: debug\n"
}
# Inline config should still point to the managed path.
assert {
condition = output.agent_firewall_config_path == "$HOME/.coder-modules/coder/agent-firewall/config/config.yaml"
error_message = "agent_firewall_config_path output should point to managed config path"
}
}
run "plan_with_config_path" {
command = plan
variables {
agent_id = "test-agent-id"
agent_firewall_config_path = "/workspace/my-boundary-config.yaml"
}
# agent_firewall_config_path output should point to the user-provided path.
assert {
condition = output.agent_firewall_config_path == "/workspace/my-boundary-config.yaml"
error_message = "agent_firewall_config_path output should point to user-provided path"
}
}
run "plan_with_both_configs_should_fail" {
command = plan
variables {
agent_id = "test-agent-id"
agent_firewall_config = "allowlist: []"
agent_firewall_config_path = "/workspace/config.yaml"
}
expect_failures = [
var.agent_firewall_config,
]
}

View File

@ -1,218 +0,0 @@
allowlist:
- domain=${CODER_DOMAIN}
# Anthropic Services
- domain=api.anthropic.com
- domain=statsig.anthropic.com
- domain=claude.ai
# OpenAI Services
- domain=api.openai.com
- domain=platform.openai.com
- domain=openai.com
- domain=chatgpt.com
- domain=*.oaiusercontent.com
- domain=*.oaistatic.com
# Version Control
- domain=github.com
- domain=www.github.com
- domain=api.github.com
- domain=raw.githubusercontent.com
- domain=objects.githubusercontent.com
- domain=codeload.github.com
- domain=avatars.githubusercontent.com
- domain=camo.githubusercontent.com
- domain=gist.github.com
- domain=gitlab.com
- domain=www.gitlab.com
- domain=registry.gitlab.com
- domain=bitbucket.org
- domain=www.bitbucket.org
- domain=api.bitbucket.org
# Container Registries
- domain=registry-1.docker.io
- domain=auth.docker.io
- domain=index.docker.io
- domain=hub.docker.com
- domain=www.docker.com
- domain=production.cloudflare.docker.com
- domain=download.docker.com
- domain=*.gcr.io
- domain=ghcr.io
- domain=mcr.microsoft.com
- domain=*.data.mcr.microsoft.com
# Cloud Platforms
- domain=cloud.google.com
- domain=accounts.google.com
- domain=gcloud.google.com
- domain=*.googleapis.com
- domain=storage.googleapis.com
- domain=compute.googleapis.com
- domain=container.googleapis.com
- domain=azure.com
- domain=portal.azure.com
- domain=microsoft.com
- domain=www.microsoft.com
- domain=*.microsoftonline.com
- domain=packages.microsoft.com
- domain=dotnet.microsoft.com
- domain=dot.net
- domain=visualstudio.com
- domain=dev.azure.com
- domain=oracle.com
- domain=www.oracle.com
- domain=java.com
- domain=www.java.com
- domain=java.net
- domain=www.java.net
- domain=download.oracle.com
- domain=yum.oracle.com
# Package Managers - JavaScript/Node
- domain=registry.npmjs.org
- domain=www.npmjs.com
- domain=www.npmjs.org
- domain=npmjs.com
- domain=npmjs.org
- domain=yarnpkg.com
- domain=registry.yarnpkg.com
# Package Managers - Python
- domain=pypi.org
- domain=www.pypi.org
- domain=files.pythonhosted.org
- domain=pythonhosted.org
- domain=test.pypi.org
- domain=pypi.python.org
- domain=pypa.io
- domain=www.pypa.io
# Package Managers - Ruby
- domain=rubygems.org
- domain=www.rubygems.org
- domain=api.rubygems.org
- domain=index.rubygems.org
- domain=ruby-lang.org
- domain=www.ruby-lang.org
- domain=rubyforge.org
- domain=www.rubyforge.org
- domain=rubyonrails.org
- domain=www.rubyonrails.org
- domain=rvm.io
- domain=get.rvm.io
# Package Managers - Rust
- domain=crates.io
- domain=www.crates.io
- domain=static.crates.io
- domain=rustup.rs
- domain=static.rust-lang.org
- domain=www.rust-lang.org
# Package Managers - Go
- domain=proxy.golang.org
- domain=sum.golang.org
- domain=index.golang.org
- domain=golang.org
- domain=www.golang.org
- domain=go.dev
- domain=dl.google.com
- domain=goproxy.io
- domain=pkg.go.dev
# Package Managers - JVM
- domain=maven.org
- domain=repo.maven.org
- domain=central.maven.org
- domain=repo1.maven.org
- domain=jcenter.bintray.com
- domain=gradle.org
- domain=www.gradle.org
- domain=services.gradle.org
- domain=spring.io
- domain=repo.spring.io
# Package Managers - Other Languages
- domain=packagist.org
- domain=www.packagist.org
- domain=repo.packagist.org
- domain=nuget.org
- domain=www.nuget.org
- domain=api.nuget.org
- domain=pub.dev
- domain=api.pub.dev
- domain=hex.pm
- domain=www.hex.pm
- domain=cpan.org
- domain=www.cpan.org
- domain=metacpan.org
- domain=www.metacpan.org
- domain=api.metacpan.org
- domain=cocoapods.org
- domain=www.cocoapods.org
- domain=cdn.cocoapods.org
- domain=haskell.org
- domain=www.haskell.org
- domain=hackage.haskell.org
- domain=swift.org
- domain=www.swift.org
# Linux Distributions
- domain=archive.ubuntu.com
- domain=security.ubuntu.com
- domain=ubuntu.com
- domain=www.ubuntu.com
- domain=*.ubuntu.com
- domain=ppa.launchpad.net
- domain=launchpad.net
- domain=www.launchpad.net
# Development Tools & Platforms
- domain=dl.k8s.io
- domain=pkgs.k8s.io
- domain=k8s.io
- domain=www.k8s.io
- domain=releases.hashicorp.com
- domain=apt.releases.hashicorp.com
- domain=rpm.releases.hashicorp.com
- domain=archive.releases.hashicorp.com
- domain=hashicorp.com
- domain=www.hashicorp.com
- domain=repo.anaconda.com
- domain=conda.anaconda.org
- domain=anaconda.org
- domain=www.anaconda.com
- domain=anaconda.com
- domain=continuum.io
- domain=apache.org
- domain=www.apache.org
- domain=archive.apache.org
- domain=downloads.apache.org
- domain=eclipse.org
- domain=www.eclipse.org
- domain=download.eclipse.org
- domain=nodejs.org
- domain=www.nodejs.org
# Cloud Services & Monitoring
- domain=statsig.com
- domain=www.statsig.com
- domain=api.statsig.com
- domain=*.sentry.io
# Content Delivery & Mirrors
- domain=*.sourceforge.net
- domain=packagecloud.io
- domain=*.packagecloud.io
# Schema & Configuration
- domain=json-schema.org
- domain=www.json-schema.org
- domain=json.schemastore.org
- domain=www.schemastore.org
log_dir: ${BOUNDARY_LOG_DIR}
log_level: warn
proxy_port: 8087

View File

@ -1,376 +0,0 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import {
execContainer,
readFileContainer,
runTerraformInit,
runTerraformApply,
testRequiredVariables,
runContainer,
removeContainer,
} from "~test";
import {
loadTestFile,
writeExecutable,
execModuleScript,
extractCoderEnvVars,
} from "../agentapi/test-util";
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 {
moduleVariables?: Record<string, string>;
skipCoderMock?: boolean;
}
const MODULE_DIR = "/home/coder/.coder-modules/coder/agent-firewall";
const CONFIG_PATH = `${MODULE_DIR}/config/config.yaml`;
const WRAPPER_PATH = `${MODULE_DIR}/scripts/agent-firewall-wrapper.sh`;
const setup = async (
props?: SetupProps,
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
...props?.moduleVariables,
});
const coderEnvVars = extractCoderEnvVars(state);
const id = await runContainer("codercom/enterprise-node:latest");
registerCleanup(async () => {
await removeContainer(id);
});
await execContainer(id, ["bash", "-c", "mkdir -p /home/coder/project"]);
// Create a mock coder binary with boundary subcommand and exp sync support
if (!props?.skipCoderMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: await loadTestFile(import.meta.dir, "coder-mock.sh"),
});
}
// Extract ALL coder_scripts from the state (coder-utils creates multiple)
const allScripts = state.resources
.filter((r) => r.type === "coder_script")
.map((r) => ({
name: r.name,
script: r.instances[0].attributes.script as string,
}));
// Run scripts in lifecycle order
const executionOrder = [
"pre_install_script",
"install_script",
"post_install_script",
];
const orderedScripts = executionOrder
.map((name) => allScripts.find((s) => s.name === name))
.filter((s): s is NonNullable<typeof s> => s != null);
// Write each script individually and create a combined runner
const scriptPaths: string[] = [];
for (const s of orderedScripts) {
const scriptPath = `/home/coder/${s.name}.sh`;
await writeExecutable({
containerId: id,
filePath: scriptPath,
content: s.script,
});
scriptPaths.push(scriptPath);
}
const combinedScript = [
"#!/bin/bash",
"set -o errexit",
"set -o pipefail",
...scriptPaths.map((p) => `bash "${p}"`),
].join("\n");
await writeExecutable({
containerId: id,
filePath: "/home/coder/script.sh",
content: combinedScript,
});
return { id, coderEnvVars };
};
setDefaultTimeout(60 * 1000);
describe("agent-firewall", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent-id",
});
test("terraform-state-basic", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-id",
});
const resources = state.resources;
// No coder_env resources should exist
const envResources = resources.filter((r) => r.type === "coder_env");
expect(envResources).toHaveLength(0);
// Verify no env vars are exported
const coderEnvVars = extractCoderEnvVars(state);
expect(coderEnvVars["BOUNDARY_WRAPPER_PATH"]).toBeUndefined();
expect(coderEnvVars["BOUNDARY_CONFIG"]).toBeUndefined();
// Verify agent_firewall_config_path output
expect(state.outputs["agent_firewall_config_path"]?.value).toBe(
"$HOME/.coder-modules/coder/agent-firewall/config/config.yaml",
);
// Verify agent_firewall_wrapper_path output
expect(state.outputs["agent_firewall_wrapper_path"]?.value).toBe(
"$HOME/.coder-modules/coder/agent-firewall/scripts/agent-firewall-wrapper.sh",
);
// Verify scripts output contains install script
const scripts = state.outputs["scripts"]?.value as string[];
expect(scripts).toContain("coder-agent-firewall-install_script");
});
test("terraform-state-custom-module-directory", async () => {
const customDir = "$HOME/.coder-modules/custom/agent-firewall";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-id",
module_directory: customDir,
});
// Verify output uses custom dir
const outputs = state.outputs;
expect(outputs["agent_firewall_wrapper_path"]?.value).toBe(
`${customDir}/scripts/agent-firewall-wrapper.sh`,
);
// Config path follows module directory
expect(outputs["agent_firewall_config_path"]?.value).toBe(
`${customDir}/config/config.yaml`,
);
});
test("terraform-state-inline-config", async () => {
const inlineConfig =
"allowlist:\n - domain=example.com\nlog_level: debug\n";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-id",
agent_firewall_config: inlineConfig,
});
// Inline config still writes to the managed path.
expect(state.outputs["agent_firewall_config_path"]?.value).toBe(
"$HOME/.coder-modules/coder/agent-firewall/config/config.yaml",
);
});
test("terraform-state-config-path", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent-id",
agent_firewall_config_path: "/workspace/my-config.yaml",
});
// agent_firewall_config_path output should point to the user-provided path.
expect(state.outputs["agent_firewall_config_path"]?.value).toBe(
"/workspace/my-config.yaml",
);
});
test("happy-path-coder-subcommand", async () => {
const { id } = await setup();
await execModuleScript(id);
// Verify the wrapper script was created
const wrapperContent = await readFileContainer(id, WRAPPER_PATH);
expect(wrapperContent).toContain("#!/usr/bin/env bash");
expect(wrapperContent).toContain("coder-no-caps");
expect(wrapperContent).toContain("boundary");
// Verify the wrapper script is executable
const statResult = await execContainer(id, [
"stat",
"-c",
"%a",
WRAPPER_PATH,
]);
expect(statResult.stdout.trim()).toMatch(/7[0-9][0-9]/);
// Verify coder-no-caps binary was created
const coderNoCapsResult = await execContainer(id, [
"test",
"-f",
`${MODULE_DIR}/scripts/coder-no-caps`,
]);
expect(coderNoCapsResult.exitCode).toBe(0);
// Verify default boundary config was written inside module directory
const configContent = await readFileContainer(id, CONFIG_PATH);
expect(configContent).toContain("allowlist:");
expect(configContent).toContain("domain=api.anthropic.com");
expect(configContent).toContain("domain=api.openai.com");
expect(configContent).toContain("proxy_port: 8087");
// Verify Coder domain was auto-filled from data.coder_workspace.me
// (the placeholder should be replaced with the actual deployment domain).
expect(configContent).not.toContain("domain=your-deployment.coder.com");
// Verify $HOME was expanded in log_dir (should be absolute, not literal $HOME).
expect(configContent).toContain("log_dir: /home/coder/");
expect(configContent).not.toContain("$HOME");
// Check install log
const installLog = await readFileContainer(
id,
`${MODULE_DIR}/logs/install.log`,
);
expect(installLog).toContain("Using coder boundary subcommand");
expect(installLog).toContain("Boundary config written to");
expect(installLog).toContain("boundary wrapper configured");
});
test("inline-config-written", async () => {
const customConfig =
"allowlist:\n - domain=custom.example.com\nlog_level: info\n";
const { id } = await setup({
moduleVariables: {
agent_firewall_config: customConfig,
},
});
await execModuleScript(id);
// Verify the inline config was written
const configContent = await readFileContainer(id, CONFIG_PATH);
expect(configContent).toContain("domain=custom.example.com");
expect(configContent).toContain("log_level: info");
});
test("config-path-skips-write", async () => {
const { id } = await setup({
moduleVariables: {
agent_firewall_config_path: "/workspace/external-config.yaml",
},
});
await execModuleScript(id);
// Verify NO config was written to the default path
const checkResult = await execContainer(id, ["test", "-f", CONFIG_PATH]);
expect(checkResult.exitCode).not.toBe(0);
// Check install log confirms skip
const installLog = await readFileContainer(
id,
`${MODULE_DIR}/logs/install.log`,
);
expect(installLog).toContain(
"Using external boundary config, skipping config write",
);
});
// Note: Tests for use_agent_firewall_directly and
// compile_agent_firewall_from_source are skipped because they require
// network access (downloading boundary) or compilation which are too
// slow for unit tests. These modes are tested manually.
test("custom-hooks", async () => {
const preInstallMarker = "pre-install-executed";
const postInstallMarker = "post-install-executed";
const { id } = await setup({
moduleVariables: {
pre_install_script: `#!/bin/bash\necho '${preInstallMarker}'`,
post_install_script: `#!/bin/bash\necho '${postInstallMarker}'`,
},
});
await execModuleScript(id);
// Verify pre-install script ran
const preInstallLog = await readFileContainer(
id,
`${MODULE_DIR}/logs/pre_install.log`,
);
expect(preInstallLog).toContain(preInstallMarker);
// Verify post-install script ran
const postInstallLog = await readFileContainer(
id,
`${MODULE_DIR}/logs/post_install.log`,
);
expect(postInstallLog).toContain(postInstallMarker);
// Verify main install still ran
const installLog = await readFileContainer(
id,
`${MODULE_DIR}/logs/install.log`,
);
expect(installLog).toContain("boundary wrapper configured");
});
test("no-env-vars", async () => {
const { coderEnvVars } = await setup();
// No env vars should be exported by this module.
expect(coderEnvVars["BOUNDARY_WRAPPER_PATH"]).toBeUndefined();
expect(coderEnvVars["BOUNDARY_CONFIG"]).toBeUndefined();
});
test("wrapper-script-execution", async () => {
const { id } = await setup();
await execModuleScript(id);
// Try executing the wrapper script with a command
const wrapperResult = await execContainer(id, [
"bash",
"-c",
`${WRAPPER_PATH} echo boundary-test`,
]);
// The wrapper passes the command directly to the boundary command
expect(wrapperResult.stdout).toContain("boundary-test");
});
test("installation-idempotency", async () => {
const { id } = await setup();
// Run the installation twice
await execModuleScript(id);
const firstInstallLog = await readFileContainer(
id,
`${MODULE_DIR}/logs/install.log`,
);
// Run again
const secondRun = await execModuleScript(id);
expect(secondRun.exitCode).toBe(0);
// Both runs should succeed
expect(firstInstallLog).toContain("boundary wrapper configured");
});
});

View File

@ -1,128 +0,0 @@
terraform {
required_version = ">= 1.9"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
data "coder_workspace" "me" {}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "agent_firewall_version" {
type = string
description = "Agent firewall version. When use_agent_firewall_directly is true, a release version should be provided or 'latest' for the latest release. When compile_agent_firewall_from_source is true, a valid git reference should be provided (tag, commit, branch)."
default = "latest"
}
variable "compile_agent_firewall_from_source" {
type = bool
description = "Whether to compile agent-firewall from source instead of using the official install script."
default = false
}
variable "use_agent_firewall_directly" {
type = bool
description = "Whether to use agent-firewall binary directly instead of `coder boundary` subcommand. When false (default), uses `coder boundary` subcommand. When true, installs and uses agent-firewall binary from release."
default = false
}
variable "agent_firewall_config" {
type = string
description = "Inline agent-firewall configuration content (YAML). Overrides the module's default config. Mutually exclusive with agent_firewall_config_path."
default = null
validation {
condition = !(var.agent_firewall_config != null && var.agent_firewall_config_path != null)
error_message = "Only one of agent_firewall_config or agent_firewall_config_path may be set."
}
}
variable "agent_firewall_config_path" {
type = string
description = "Path to an existing agent-firewall config file in the workspace. When set, no config is written and the agent_firewall_config_path output points to this path. Mutually exclusive with agent_firewall_config."
default = null
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing agent-firewall."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing agent-firewall."
default = null
}
variable "module_directory" {
type = string
description = "Directory where the agent-firewall module scripts will be located. Default is $HOME/.coder-modules/coder/agent-firewall."
default = "$HOME/.coder-modules/coder/agent-firewall"
}
locals {
boundary_wrapper_path = "${var.module_directory}/scripts/agent-firewall-wrapper.sh"
# Extract domain from the Coder access URL for the default config
# allowlist (e.g., "https://dev.coder.com/" -> "dev.coder.com").
coder_domain = try(regex("^https?://([^/:]+)", data.coder_workspace.me.access_url)[0], "")
# Config handling: resolve which config content to write and where
# agent_firewall_config_path output points to.
default_boundary_config = templatefile("${path.module}/config.yaml.tftpl", {
CODER_DOMAIN = local.coder_domain
BOUNDARY_LOG_DIR = "${var.module_directory}/logs/agent_firewall_logs"
})
boundary_config_content = var.agent_firewall_config != null ? var.agent_firewall_config : local.default_boundary_config
boundary_config_dir = "${var.module_directory}/config"
boundary_config_file_path = "${local.boundary_config_dir}/config.yaml"
effective_boundary_config_path = var.agent_firewall_config_path != null ? var.agent_firewall_config_path : local.boundary_config_file_path
write_boundary_config = var.agent_firewall_config_path == null
install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
BOUNDARY_VERSION = var.agent_firewall_version
COMPILE_BOUNDARY_FROM_SOURCE = tostring(var.compile_agent_firewall_from_source)
USE_BOUNDARY_DIRECTLY = tostring(var.use_agent_firewall_directly)
MODULE_DIR = var.module_directory
BOUNDARY_WRAPPER_PATH = local.boundary_wrapper_path
WRITE_BOUNDARY_CONFIG = tostring(local.write_boundary_config)
BOUNDARY_CONFIG_CONTENT_B64 = local.write_boundary_config ? base64encode(local.boundary_config_content) : ""
BOUNDARY_CONFIG_DIR = local.boundary_config_dir
BOUNDARY_CONFIG_FILE = local.boundary_config_file_path
})
}
module "coder_utils" {
source = "registry.coder.com/coder/coder-utils/coder"
version = "0.0.1"
agent_id = var.agent_id
display_name_prefix = "Agent Firewall"
module_directory = var.module_directory
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
install_script = local.install_script
}
output "agent_firewall_wrapper_path" {
description = "Path to the agent-firewall wrapper script."
value = local.boundary_wrapper_path
}
output "agent_firewall_config_path" {
description = "Effective path to the agent-firewall config file."
value = local.effective_boundary_config_path
}
output "scripts" {
description = "List of script names for coder exp sync coordination."
value = module.coder_utils.scripts
}

View File

@ -1,131 +0,0 @@
#!/bin/bash
# Sets up boundary for network isolation in Coder workspaces.
set -euo pipefail
BOUNDARY_VERSION='${BOUNDARY_VERSION}'
COMPILE_BOUNDARY_FROM_SOURCE='${COMPILE_BOUNDARY_FROM_SOURCE}'
USE_BOUNDARY_DIRECTLY='${USE_BOUNDARY_DIRECTLY}'
MODULE_DIR="${MODULE_DIR}"
BOUNDARY_WRAPPER_PATH="${BOUNDARY_WRAPPER_PATH}"
WRITE_BOUNDARY_CONFIG='${WRITE_BOUNDARY_CONFIG}'
BOUNDARY_CONFIG_CONTENT=$(echo -n '${BOUNDARY_CONFIG_CONTENT_B64}' | base64 -d | sed "s|\$HOME|$HOME|g")
BOUNDARY_CONFIG_DIR="${BOUNDARY_CONFIG_DIR}"
BOUNDARY_CONFIG_FILE="${BOUNDARY_CONFIG_FILE}"
printf "BOUNDARY_VERSION: %s\n" "$${BOUNDARY_VERSION}"
printf "COMPILE_BOUNDARY_FROM_SOURCE: %s\n" "$${COMPILE_BOUNDARY_FROM_SOURCE}"
printf "USE_BOUNDARY_DIRECTLY: %s\n" "$${USE_BOUNDARY_DIRECTLY}"
printf "MODULE_DIR: %s\n" "$${MODULE_DIR}"
printf "BOUNDARY_WRAPPER_PATH: %s\n" "$${BOUNDARY_WRAPPER_PATH}"
printf "WRITE_BOUNDARY_CONFIG: %s\n" "$${WRITE_BOUNDARY_CONFIG}"
printf "BOUNDARY_CONFIG_DIR: %s\n" "$${BOUNDARY_CONFIG_DIR}"
printf "BOUNDARY_CONFIG_FILE: %s\n" "$${BOUNDARY_CONFIG_FILE}"
validate_boundary_subcommand() {
if ! command -v coder > /dev/null 2>&1; then
echo "Error: 'coder' command not found. boundary cannot be enabled." >&2
exit 1
fi
local output
echo "Checking for license"
if ! output=$(coder boundary 2>&1); then
if echo "$${output}" | grep -qi "license is not entitled"; then
echo "Error: your Coder deployment is not licensed for the boundary feature." >&2
echo "$${output}" >&2
echo "" >&2
exit 1
fi
fi
}
# Install boundary binary if needed.
# Uses one of three strategies:
# 1. Compile from source (compile_boundary_from_source=true)
# 2. Install from release (use_boundary_directly=true)
# 3. Use coder boundary subcommand (default, no installation needed)
install_boundary() {
if [[ "$${COMPILE_BOUNDARY_FROM_SOURCE}" = "true" ]]; then
echo "Compiling boundary from source (version: $${BOUNDARY_VERSION})"
# Remove existing boundary directory to allow re-running safely
if [[ -d boundary ]]; then
rm -rf boundary
fi
echo "Cloning boundary repository"
git clone https://github.com/coder/boundary.git
cd boundary || exit 1
git checkout "$${BOUNDARY_VERSION}"
make build
sudo cp boundary /usr/local/bin/
sudo chmod +x /usr/local/bin/boundary
cd - || exit 1
elif [[ "$${USE_BOUNDARY_DIRECTLY}" = "true" ]]; then
echo "Installing boundary using official install script (version: $${BOUNDARY_VERSION})"
curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$${BOUNDARY_VERSION}"
else
validate_boundary_subcommand
echo "Using coder boundary subcommand (provided by Coder)"
fi
}
# Write boundary config file if the module is responsible for it.
write_boundary_config() {
if [[ "$${WRITE_BOUNDARY_CONFIG}" != "true" ]]; then
echo "Using external boundary config, skipping config write."
return 0
fi
mkdir -p "$${BOUNDARY_CONFIG_DIR}"
echo "$${BOUNDARY_CONFIG_CONTENT}" > "$${BOUNDARY_CONFIG_FILE}"
echo "Boundary config written to $${BOUNDARY_CONFIG_FILE}"
}
# Set up boundary: install, write config, create wrapper script.
setup_boundary() {
echo "Setting up coder boundary..."
# Install boundary binary if needed
install_boundary
# Write boundary config
write_boundary_config
# Ensure the wrapper script directory exists.
mkdir -p "$(dirname "$${BOUNDARY_WRAPPER_PATH}")"
if [[ "$${COMPILE_BOUNDARY_FROM_SOURCE}" = "true" ]] || [[ "$${USE_BOUNDARY_DIRECTLY}" = "true" ]]; then
# Use boundary binary directly (from compilation or release installation)
cat > "$${BOUNDARY_WRAPPER_PATH}" << 'WRAPPER_EOF'
#!/usr/bin/env bash
set -euo pipefail
exec boundary "$@"
WRAPPER_EOF
else
# Use coder boundary subcommand (default)
# Copy coder binary to strip CAP_NET_ADMIN capabilities.
# This is necessary because boundary doesn't work with privileged binaries
# (you can't launch privileged binaries inside network namespaces unless
# you have sys_admin).
CODER_NO_CAPS="$${MODULE_DIR}/scripts/coder-no-caps"
if ! cp "$(command -v coder)" "$${CODER_NO_CAPS}"; then
echo "Error: Failed to copy coder binary to $${CODER_NO_CAPS}. boundary cannot be enabled." >&2
exit 1
fi
cat > "$${BOUNDARY_WRAPPER_PATH}" << 'WRAPPER_EOF'
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$${BASH_SOURCE[0]}")" && pwd)"
exec "$${SCRIPT_DIR}/coder-no-caps" boundary "$@"
WRAPPER_EOF
fi
chmod +x "$${BOUNDARY_WRAPPER_PATH}"
echo "boundary wrapper configured: $${BOUNDARY_WRAPPER_PATH}"
}
setup_boundary

View File

@ -1,38 +0,0 @@
#!/bin/bash
# Mock coder command for testing boundary module
# Handles: coder boundary [--help | <command>]
# Handles: coder exp sync [want|start|complete] (no-op for testing)
# Handle exp sync commands (no-op for testing)
if [[ "$1" == "exp" ]] && [[ "$2" == "sync" ]]; then
exit 0
fi
if [[ "$1" == "boundary" ]]; then
shift
# Handle --help flag
if [[ "$1" == "--help" ]]; then
cat << 'EOF'
boundary - Run commands in network isolation
Usage:
coder boundary [flags] -- <command> [args...]
Examples:
coder boundary -- curl https://example.com
coder boundary -- npm install
Flags:
-h, --help help for boundary
EOF
exit 0
fi
# Execute the remaining arguments as a command
exec "$@"
fi
echo "Mock coder: Unknown command: $*"
exit 1

View File

@ -13,7 +13,7 @@ Install and configure the [Claude Code](https://docs.anthropic.com/en/docs/agent
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
anthropic_api_key = "xxxx-xxxxx-xxxx"
}
@ -47,7 +47,7 @@ locals {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = local.claude_workdir
anthropic_api_key = "xxxx-xxxxx-xxxx"
@ -78,7 +78,7 @@ resource "coder_app" "claude" {
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_ai_gateway = true
@ -95,33 +95,6 @@ Claude Code then routes API requests through Coder's AI Gateway instead of direc
> [!CAUTION]
> `enable_ai_gateway = true` is mutually exclusive with `anthropic_api_key` and `claude_code_oauth_token`. Setting any of them together fails at plan time.
### Enterprise policy via managed settings
The `managed_settings` input writes a policy file to `/etc/claude-code/managed-settings.d/10-coder.json` inside the workspace. Claude Code reads this directory at startup with the highest configuration precedence, so users cannot override these values in their own `~/.claude/settings.json`. This is a local file mechanism and works with any inference backend (Anthropic API, AWS Bedrock, Google Vertex AI, or AI Gateway).
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
anthropic_api_key = "xxxx-xxxxx-xxxx"
managed_settings = {
permissions = {
defaultMode = "acceptEdits"
disableBypassPermissionsMode = "disable"
deny = ["Bash(curl:*)", "Bash(wget:*)", "WebFetch"]
}
env = {
DISABLE_TELEMETRY = "0"
}
}
}
```
See the [Claude Code settings reference](https://docs.anthropic.com/en/docs/claude-code/settings) for the full schema. Common keys: `permissions` (`defaultMode`, `allow`, `deny`, `disableBypassPermissionsMode`, `additionalDirectories`), `env`, `model`, `apiKeyHelper`, `hooks`, `cleanupPeriodDays`.
### Advanced Configuration
This example shows version pinning, a pre-installed binary path, a custom model, and MCP servers.
@ -129,7 +102,7 @@ This example shows version pinning, a pre-installed binary path, a custom model,
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
@ -193,7 +166,7 @@ Downstream `coder_script` resources can wait for this module's install pipeline
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
anthropic_api_key = "xxxx-xxxxx-xxxx"
@ -279,7 +252,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@ -336,7 +309,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
@ -377,7 +350,7 @@ The module automatically tags every span and metric with `coder.workspace_id`, `
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "5.2.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
anthropic_api_key = "xxxx-xxxxx-xxxx"

View File

@ -382,13 +382,10 @@ describe("claude-code", async () => {
const parsed = JSON.parse(claudeConfig);
expect(parsed.autoUpdaterStatus).toBe("disabled");
expect(parsed.hasCompletedOnboarding).toBe(true);
expect(parsed.bypassPermissionsModeAccepted).toBe(true);
expect(parsed.hasAcknowledgedCostThreshold).toBe(true);
expect(parsed.projects[workdir].hasCompletedProjectOnboarding).toBe(true);
expect(parsed.projects[workdir].hasTrustDialogAccepted).toBe(true);
// Permission posture is delivered via /etc/claude-code/managed-settings.d/,
// not user-writable ~/.claude.json acceptance flags.
expect(parsed.bypassPermissionsModeAccepted).toBeUndefined();
expect(parsed.autoModeAccepted).toBeUndefined();
});
test("standalone-mode-with-oauth-token", async () => {
@ -416,7 +413,7 @@ describe("claude-code", async () => {
);
const parsed = JSON.parse(claudeConfig);
expect(parsed.hasCompletedOnboarding).toBe(true);
expect(parsed.bypassPermissionsModeAccepted).toBeUndefined();
expect(parsed.bypassPermissionsModeAccepted).toBe(true);
});
test("standalone-mode-no-auth", async () => {
@ -439,49 +436,6 @@ describe("claude-code", async () => {
expect(resp.stdout.trim()).toBe("ABSENT");
});
test("claude-managed-settings-written", async () => {
const { id, scripts } = await setup({
moduleVariables: {
managed_settings: JSON.stringify({
permissions: {
defaultMode: "acceptEdits",
disableBypassPermissionsMode: "disable",
deny: ["Bash(rm -rf*)"],
},
}),
},
});
await runScripts(id, scripts);
const policy = await execContainer(id, [
"bash",
"-c",
"cat /etc/claude-code/managed-settings.d/10-coder.json",
]);
expect(policy.exitCode).toBe(0);
expect(policy.stdout).toContain('"defaultMode":"acceptEdits"');
expect(policy.stdout).toContain('"disableBypassPermissionsMode":"disable"');
expect(policy.stdout).toContain('"deny":["Bash(rm -rf*)"]');
const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder/claude-code/logs/install.log",
);
expect(installLog).toContain("Wrote Claude Code managed settings");
});
test("claude-managed-settings-not-set", async () => {
const { id, scripts } = await setup();
await runScripts(id, scripts);
const resp = await execContainer(id, [
"bash",
"-c",
"test -e /etc/claude-code/managed-settings.d/10-coder.json && echo EXISTS || echo ABSENT",
]);
expect(resp.stdout.trim()).toBe("ABSENT");
});
test("telemetry-otel", async () => {
const { coderEnvVars } = await setup({
moduleVariables: {

View File

@ -102,12 +102,6 @@ variable "claude_binary_path" {
}
}
variable "managed_settings" {
type = any
description = "Policy settings written to /etc/claude-code/managed-settings.d/10-coder.json. Highest-precedence client config; works with any inference backend (Anthropic API, Bedrock, Vertex, AI Gateway). See https://docs.anthropic.com/en/docs/claude-code/settings for the schema."
default = null
}
variable "enable_ai_gateway" {
type = bool
description = "Use AI Gateway for Claude Code. https://coder.com/docs/ai-coder/ai-gateway"
@ -136,6 +130,12 @@ variable "telemetry" {
description = "Configure Claude Code OpenTelemetry export. When enabled, sets CLAUDE_CODE_ENABLE_TELEMETRY and the standard OTEL_EXPORTER_OTLP_* environment variables. Coder workspace identifiers (coder.workspace_id, coder.workspace_name, coder.workspace_owner, coder.template_name) are automatically appended to OTEL_RESOURCE_ATTRIBUTES so Claude Code telemetry can be joined with Coder audit and exectrace logs."
}
variable "module_directory" {
type = string
description = "The directory where the module is installed. Defaults to $HOME/.coder-modules/coder/claude-code."
default = "$HOME/.coder-modules/coder/claude-code"
}
resource "coder_env" "claude_code_oauth_token" {
count = var.claude_code_oauth_token != "" ? 1 : 0
agent_id = var.agent_id
@ -243,9 +243,7 @@ locals {
ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : ""
ARG_MCP_CONFIG_REMOTE_PATH = base64encode(jsonencode(var.mcp_config_remote_path))
ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway)
ARG_MANAGED_SETTINGS_JSON = var.managed_settings != null ? base64encode(jsonencode(var.managed_settings)) : ""
})
module_dir_name = ".coder-modules/coder/claude-code"
}
module "coder_utils" {
@ -253,7 +251,7 @@ module "coder_utils" {
version = "0.0.1"
agent_id = var.agent_id
module_directory = "$HOME/${local.module_dir_name}"
module_directory = var.module_directory
display_name_prefix = "Claude Code"
icon = var.icon
pre_install_script = var.pre_install_script
@ -268,3 +266,11 @@ output "scripts" {
description = "Ordered list of coder exp sync names for the coder_script resources this module actually creates, in run order (pre_install, install, post_install). Scripts that were not configured are absent from the list."
value = module.coder_utils.scripts
}
output "agent_reference" {
value = {
agent_ref = "claude-code"
agent_module_dir = var.module_directory
agent_binary_path = concat(var.claude_binary_path, "/claude")
}
}

View File

@ -283,47 +283,3 @@ run "test_workdir_optional" {
error_message = "workdir should default to null when omitted"
}
}
run "test_managed_settings" {
command = plan
variables {
agent_id = "test-agent-managed-settings"
workdir = "/home/coder/project"
managed_settings = {
permissions = {
defaultMode = "acceptEdits"
disableBypassPermissionsMode = "disable"
deny = ["Bash(rm -rf*)"]
}
}
}
assert {
condition = var.managed_settings.permissions.defaultMode == "acceptEdits"
error_message = "managed_settings should accept the permissions object"
}
assert {
condition = strcontains(local.install_script, "/etc/claude-code/managed-settings.d")
error_message = "install script should reference the managed-settings.d drop-in directory"
}
assert {
condition = strcontains(local.install_script, base64encode(jsonencode(var.managed_settings)))
error_message = "install script should embed the base64-encoded managed_settings JSON"
}
}
run "test_managed_settings_default_null" {
command = plan
variables {
agent_id = "test-agent-managed-settings-default"
}
assert {
condition = var.managed_settings == null
error_message = "managed_settings should default to null when omitted"
}
}

View File

@ -17,7 +17,6 @@ ARG_CLAUDE_BINARY_PATH="$${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}"
ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d)
ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n '${ARG_MCP_CONFIG_REMOTE_PATH}' | base64 -d)
ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}'
ARG_MANAGED_SETTINGS_JSON=$(echo -n '${ARG_MANAGED_SETTINGS_JSON}' | base64 -d)
export PATH="$${ARG_CLAUDE_BINARY_PATH}:$PATH"
@ -30,7 +29,6 @@ printf "ARG_CLAUDE_BINARY_PATH: %s\n" "$${ARG_CLAUDE_BINARY_PATH}"
printf "ARG_MCP: %s\n" "$${ARG_MCP}"
printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$${ARG_MCP_CONFIG_REMOTE_PATH}"
printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}"
printf "ARG_MANAGED_SETTINGS_JSON: %s\n" "$${ARG_MANAGED_SETTINGS_JSON}"
echo "--------------------------------"
@ -146,32 +144,6 @@ function setup_claude_configurations() {
}
function write_managed_settings() {
if [ -z "$${ARG_MANAGED_SETTINGS_JSON}" ]; then
return
fi
local dropin_dir="/etc/claude-code/managed-settings.d"
local target="$${dropin_dir}/10-coder.json"
if ! echo "$${ARG_MANAGED_SETTINGS_JSON}" | jq empty 2> /dev/null; then
echo "Warning: managed_settings is not valid JSON, skipping policy write"
return
fi
if command_exists sudo; then
sudo mkdir -p "$${dropin_dir}"
echo "$${ARG_MANAGED_SETTINGS_JSON}" | sudo tee "$${target}" > /dev/null
sudo chmod 0644 "$${target}"
else
mkdir -p "$${dropin_dir}"
echo "$${ARG_MANAGED_SETTINGS_JSON}" > "$${target}"
chmod 0644 "$${target}"
fi
echo "Wrote Claude Code managed settings to $${target}"
}
function configure_standalone_mode() {
echo "Configuring Claude Code for standalone mode..."
@ -186,6 +158,8 @@ function configure_standalone_mode() {
echo "Updating existing Claude configuration at $${claude_config}"
jq '.autoUpdaterStatus = "disabled" |
.autoModeAccepted = true |
.bypassPermissionsModeAccepted = true |
.hasAcknowledgedCostThreshold = true |
.hasCompletedOnboarding = true' \
"$${claude_config}" > "$${claude_config}.tmp" && mv "$${claude_config}.tmp" "$${claude_config}"
@ -194,6 +168,8 @@ function configure_standalone_mode() {
cat > "$${claude_config}" << EOF
{
"autoUpdaterStatus": "disabled",
"autoModeAccepted": true,
"bypassPermissionsModeAccepted": true,
"hasAcknowledgedCostThreshold": true,
"hasCompletedOnboarding": true
}
@ -213,5 +189,4 @@ EOF
install_claude_code_cli
setup_claude_configurations
write_managed_settings
configure_standalone_mode

View File

@ -14,7 +14,7 @@ A file browser for your workspace.
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.5"
version = "1.1.4"
agent_id = coder_agent.main.id
}
```
@ -29,7 +29,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.5"
version = "1.1.4"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
@ -41,7 +41,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.5"
version = "1.1.4"
agent_id = coder_agent.main.id
database_path = ".config/filebrowser.db"
}
@ -49,13 +49,11 @@ module "filebrowser" {
### Serve from the same domain (no subdomain)
When `subdomain = false`, you must also set `agent_name` to the name of your `coder_agent` resource. Coder serves path-based apps at `/@<owner>/<workspace>.<agent>/apps/<slug>/`, so the agent name is required to build a base URL that matches the URL the user is actually browsing. If `agent_name` is omitted in this mode, `terraform apply` will fail with an explanatory error.
```tf
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.5"
version = "1.1.4"
agent_id = coder_agent.main.id
agent_name = "main"
subdomain = false

View File

@ -102,19 +102,4 @@ describe("filebrowser", async () => {
testBaseLine(output);
}, 15000);
it("fails when subdomain=false and agent_name is not provided", async () => {
let caught: Error | undefined;
try {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
subdomain: false,
});
} catch (e) {
caught = e as Error;
}
expect(caught).toBeDefined();
expect(caught!.message).toContain("agent_name");
expect(caught!.message).toContain("subdomain");
}, 15000);
});

View File

@ -20,7 +20,7 @@ data "coder_workspace_owner" "me" {}
variable "agent_name" {
type = string
description = "The name of the coder_agent resource. Required when `subdomain` is `false` so the path-based base URL matches the URL Coder serves."
description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)"
default = null
}
@ -102,13 +102,6 @@ resource "coder_script" "filebrowser" {
SERVER_BASE_PATH : local.server_base_path
})
run_on_start = true
lifecycle {
precondition {
condition = var.subdomain || var.agent_name != null
error_message = "`agent_name` is required when `subdomain` is `false`. Coder always builds path-based app URLs as `/@<owner>/<workspace>.<agent>/apps/<slug>/`, so the filebrowser base URL must include the agent name to match. Set `agent_name = \"<your coder_agent name>\"` (e.g. `\"main\"`)."
}
}
}
resource "coder_app" "filebrowser" {

View File

@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@ -43,7 +43,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
@ -105,7 +105,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
@ -125,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@ -137,7 +137,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
@ -159,7 +159,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
@ -177,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
@ -196,36 +196,13 @@ If not defined, the default, `0`, performs a full clone.
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
}
```
## Pre-clone script
Run a custom script before cloning the repository by setting the `pre_clone_script` variable.
This is useful for preparing the environment or validating prerequisites before cloning.
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
pre_clone_script = <<-EOT
#!/bin/bash
echo "Preparing to clone repository..."
# Check prerequisites
command -v npm >/dev/null 2>&1 || { echo "npm is required but not installed."; exit 1; }
# Set up environment
export NODE_ENV=development
EOT
}
```
## Post-clone script
Run a custom script after cloning the repository by setting the `post_clone_script` variable.
@ -235,7 +212,7 @@ This is useful for running initialization tasks like installing dependencies or
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.2.3"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT

View File

@ -250,59 +250,15 @@ describe("git-clone", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
base_dir: "/tmp",
post_clone_script: "echo 'Post-clone script executed'",
});
const output = await executeScriptInContainer(
state,
"alpine/git",
"sh",
"mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt",
"mkdir -p ~/fake-url && echo 'existing' > ~/fake-url/file.txt",
);
expect(output.stdout).toContain("Running post-clone script...");
expect(output.stdout).toContain("Post-clone script executed");
});
it("runs pre-clone script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
pre_clone_script: "echo 'Pre-clone script executed'",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.stdout).toContain("Running pre-clone script...");
expect(output.stdout).toContain("Pre-clone script executed");
expect(output.stdout).toContain("Cloning fake-url to ~/fake-url...");
});
it("fails when pre-clone script fails", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
pre_clone_script: "echo 'Pre-clone script failed'; exit 42",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(42);
expect(output.stdout).toContain("Running pre-clone script...");
expect(output.stdout).toContain("Pre-clone script failed");
expect(output.stdout).not.toContain("Cloning fake-url to ~/fake-url...");
});
it("fails when post-clone script fails", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
base_dir: "/tmp",
post_clone_script: "echo 'Post-clone script failed'; exit 43",
});
const output = await executeScriptInContainer(
state,
"alpine/git",
"sh",
"mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt",
);
expect(output.exitCode).toBe(43);
expect(output.stdout).toContain("Running post-clone script...");
expect(output.stdout).toContain("Post-clone script failed");
});
});

View File

@ -68,12 +68,6 @@ variable "post_clone_script" {
default = null
}
variable "pre_clone_script" {
description = "Custom script to run before cloning the repository. Runs before git clone, even if the repository already exists."
type = string
default = null
}
locals {
# Remove query parameters and fragments from the URL
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
@ -95,8 +89,6 @@ locals {
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
# Encode the post_clone_script for passing to the shell script
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
# Encode the pre_clone_script for passing to the shell script
encoded_pre_clone_script = var.pre_clone_script != null ? base64encode(var.pre_clone_script) : ""
}
output "repo_dir" {
@ -137,7 +129,6 @@ resource "coder_script" "git_clone" {
BRANCH_NAME : local.branch_name,
DEPTH = var.depth,
POST_CLONE_SCRIPT : local.encoded_post_clone_script,
PRE_CLONE_SCRIPT : local.encoded_pre_clone_script,
})
display_name = "Git Clone"
icon = "/icon/git.svg"

View File

@ -1,7 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="${REPO_URL}"
CLONE_PATH="${CLONE_PATH}"
BRANCH_NAME="${BRANCH_NAME}"
@ -9,7 +7,6 @@ BRANCH_NAME="${BRANCH_NAME}"
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
DEPTH="${DEPTH}"
POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}"
PRE_CLONE_SCRIPT="${PRE_CLONE_SCRIPT}"
# Check if the variable is empty...
if [ -z "$REPO_URL" ]; then
@ -36,16 +33,6 @@ if [ ! -d "$CLONE_PATH" ]; then
mkdir -p "$CLONE_PATH"
fi
# Run pre-clone script if provided
if [ -n "$PRE_CLONE_SCRIPT" ]; then
echo "Running pre-clone script..."
PRE_CLONE_TMP=$(mktemp)
echo "$PRE_CLONE_SCRIPT" | base64 -d > "$PRE_CLONE_TMP"
chmod +x "$PRE_CLONE_TMP"
$PRE_CLONE_TMP
rm "$PRE_CLONE_TMP"
fi
# Check if the directory is empty
# and if it is, clone the repo, otherwise skip cloning
if [ -z "$(ls -A "$CLONE_PATH")" ]; then