Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c765d9b
Add boundary support to agent api and Codex CLI Module
Mar 2, 2026
636ed84
fix: updated agent api logic and amended codex start script to suppro…
Mar 2, 2026
7b915e7
fix: resolve merge conflicts
Mar 3, 2026
444d387
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 3, 2026
350b0e7
chore: Remove codex changes to separate into another PR
Mar 3, 2026
658a90a
fix: bun prettier issue
Mar 3, 2026
9323297
Update registry/coder/modules/agentapi/scripts/main.sh
shanewhite97 Mar 3, 2026
8daa78a
Update registry/coder/modules/agentapi/scripts/main.sh
shanewhite97 Mar 3, 2026
7d7c750
feat: add validation tests for boundary
Mar 3, 2026
e24c551
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 4, 2026
3cfd67f
Merge branch 'main' of https://github.com/coder/registry into feat/ag…
Mar 4, 2026
6ec905c
Merge branch 'feat/agent-api-boundary-support' of https://github.com/…
Mar 4, 2026
18b8d41
fix: swap to using wrapper script, remove redundant variables and cop…
Mar 5, 2026
7002c97
docs: Added section for implementation of boundary in the agentapi RE…
Mar 5, 2026
7fcc6a1
fix: update tests based on new config file logic. Also ran run formatter
Mar 5, 2026
4b3c5aa
docs: add new line for README.md formatting
Mar 6, 2026
01155cc
fix: addressed latest comments on the PR. boundary_config_path remove…
Mar 6, 2026
43e578f
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 6, 2026
de2e41c
fix: formatting issues
Mar 6, 2026
98a3fb2
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 6, 2026
04e50d3
feat: provide boundary support for agent modules
Mar 6, 2026
6cbaedd
Merge branch 'feat/agent-api-boundary-support' of https://github.com/…
Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions registry/coder/modules/agentapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ module "agentapi" {
}
```

## Boundary (Network Filtering)

The agentapi module supports optional [Agent Boundaries](https://coder.com/docs/ai-coder/agent-boundaries)
for network filtering. When enabled, the module sets up a `AGENTAPI_BOUNDARY_PREFIX` environment
variable that points to a wrapper script. Agent modules should use this prefix in their
start scripts to run the agent process through boundary.

Boundary requires a `config.yaml` file with your allowlist, jail type, proxy port, and log
level. See the [Agent Boundaries documentation](https://coder.com/docs/ai-coder/agent-boundaries)
for configuration details.
To enable:

```tf
module "agentapi" {
# ... other config
enable_boundary = true
boundary_config = file("${path.module}/boundary-config.yaml")
}
```

### Contract for agent modules

When `enable_boundary = true`, the agentapi module exports `AGENTAPI_BOUNDARY_PREFIX`
as an environment variable pointing to a wrapper script. Agent module start scripts
should check for this variable and use it to prefix the agent command:

```bash
if [ -n "${AGENTAPI_BOUNDARY_PREFIX:-}" ]; then
agentapi server -- "${AGENTAPI_BOUNDARY_PREFIX}" my-agent "${ARGS[@]}" &
else
agentapi server -- my-agent "${ARGS[@]}" &
fi
```

This ensures only the agent process is sandboxed while agentapi itself runs unrestricted.

## For module developers

For a complete example of how to use this module, see the [Goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
112 changes: 112 additions & 0 deletions registry/coder/modules/agentapi/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,4 +613,116 @@ describe("agentapi", async () => {
expect(result.stdout).toContain("Sending SIGTERM to AgentAPI");
});
});

describe("boundary", async () => {
test("boundary-disabled-by-default", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
// Config file should NOT exist when boundary is disabled
const configCheck = await execContainer(id, [
"bash",
"-c",
"test -f /home/coder/.config/coder_boundary/config.yaml && echo exists || echo missing",
]);
expect(configCheck.stdout.trim()).toBe("missing");
// AGENTAPI_BOUNDARY_PREFIX should NOT be in the mock log
const mockLog = await readFileContainer(
id,
"/home/coder/agentapi-mock.log",
);
expect(mockLog).not.toContain("AGENTAPI_BOUNDARY_PREFIX:");
});

test("boundary-enabled", async () => {
const { id } = await setup({
moduleVariables: {
enable_boundary: "true",
boundary_config: `jail_type: landjail
proxy_port: 8087
log_level: warn
allowlist:
- "domain=api.example.com"`,
},
});
// Add mock coder binary for boundary setup
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: `#!/bin/bash
if [ "$1" = "boundary" ]; then
shift; shift; exec "$@"
fi
echo "mock coder"`,
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
// Config should be written to the standard location
const config = await readFileContainer(
id,
"/home/coder/.config/coder_boundary/config.yaml",
);
expect(config).toContain("jail_type: landjail");
expect(config).toContain("proxy_port: 8087");
expect(config).toContain("domain=api.example.com");
// Config should have restrictive permissions (600)
const perms = await execContainer(id, [
"bash",
"-c",
"stat -c '%a' /home/coder/.config/coder_boundary/config.yaml",
]);
expect(perms.stdout.trim()).toBe("600");
// AGENTAPI_BOUNDARY_PREFIX should be exported
const mockLog = await readFileContainer(
id,
"/home/coder/agentapi-mock.log",
);
expect(mockLog).toContain("AGENTAPI_BOUNDARY_PREFIX:");
// E2E: start script should have used the wrapper
const startLog = await readFileContainer(
id,
"/home/coder/test-agentapi-start.log",
);
expect(startLog).toContain("Starting with boundary:");
});

test("boundary-enabled-no-coder-binary", async () => {
const { id } = await setup({
moduleVariables: {
enable_boundary: "true",
boundary_config: `jail_type: landjail
proxy_port: 8087
log_level: warn`,
},
});
// Remove coder binary to simulate it not being available
await execContainer(
id,
[
"bash",
"-c",
"rm -f /usr/bin/coder /usr/local/bin/coder 2>/dev/null; hash -r",
],
["--user", "root"],
);
const resp = await execModuleScript(id);
// Script should fail because coder binary is required
expect(resp.exitCode).not.toBe(0);
const scriptLog = await readFileContainer(id, "/home/coder/script.log");
expect(scriptLog).toContain("Boundary cannot be enabled");
});

test("validate-boundary-requires-config", async () => {
expect(
setup({
moduleVariables: {
enable_boundary: "true",
// boundary_config intentionally omitted
},
}),
).rejects.toThrow(
"boundary_config is required when enable_boundary is true.",
);
});
});
});
18 changes: 18 additions & 0 deletions registry/coder/modules/agentapi/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ variable "module_dir_name" {
description = "Name of the subdirectory in the home directory for module files."
}

variable "enable_boundary" {
type = bool
description = "Enable coder boundary for network filtering. Requires boundary_config to be set."
default = false
}

variable "boundary_config" {
type = string
description = "Content of boundary config.yaml file. Required when enable_boundary is true. Must contain allowlist, jail_type, proxy_port, and log_level."
default = ""
validation {
condition = !var.enable_boundary || var.boundary_config != ""
error_message = "boundary_config is required when enable_boundary is true."
}
}

variable "enable_state_persistence" {
type = bool
description = "Enable AgentAPI conversation state persistence across restarts."
Expand Down Expand Up @@ -228,6 +244,8 @@ resource "coder_script" "agentapi" {
ARG_AGENTAPI_CHAT_BASE_PATH='${local.agentapi_chat_base_path}' \
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
ARG_BOUNDARY_CONFIG="$(echo -n '${base64encode(var.boundary_config)}' | base64 -d)" \
ARG_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \
ARG_STATE_FILE_PATH='${var.state_file_path}' \
ARG_PID_FILE_PATH='${var.pid_file_path}' \
Expand Down
38 changes: 38 additions & 0 deletions registry/coder/modules/agentapi/scripts/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}"
TASK_ID="${ARG_TASK_ID:-}"
TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}"
ENABLE_BOUNDARY="${ARG_ENABLE_BOUNDARY:-false}"
BOUNDARY_CONFIG="${ARG_BOUNDARY_CONFIG:-}"
ENABLE_STATE_PERSISTENCE="${ARG_ENABLE_STATE_PERSISTENCE:-false}"
STATE_FILE_PATH="${ARG_STATE_FILE_PATH:-}"
PID_FILE_PATH="${ARG_PID_FILE_PATH:-}"
Expand Down Expand Up @@ -109,9 +111,45 @@ export LC_ALL=en_US.UTF-8

cd "${WORKDIR}"

# Set up boundary if enabled
export AGENTAPI_BOUNDARY_PREFIX=""
if [ "${ENABLE_BOUNDARY}" = "true" ]; then
echo "Setting up coder boundary..."
# Copy the user-provided boundary config
mkdir -p ~/.config/coder_boundary
echo "Writing boundary config to ~/.config/coder_boundary/config.yaml"
echo -n "${BOUNDARY_CONFIG}" > ~/.config/coder_boundary/config.yaml
chmod 600 ~/.config/coder_boundary/config.yaml
# Copy coder binary to strip CAP_NET_ADMIN capabilities.
# This is necessary because boundary doesn't work with privileged binaries.
if command_exists coder; then
CODER_NO_CAPS="$module_path/coder-no-caps"
if cp "$(which coder)" "$CODER_NO_CAPS"; then
# Write a wrapper script to avoid word-splitting issues with exported strings.
BOUNDARY_WRAPPER_SCRIPT="$module_path/boundary-wrapper.sh"
cat > "${BOUNDARY_WRAPPER_SCRIPT}" << '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
chmod +x "${BOUNDARY_WRAPPER_SCRIPT}"
export AGENTAPI_BOUNDARY_PREFIX="${BOUNDARY_WRAPPER_SCRIPT}"
echo "Boundary wrapper configured: ${AGENTAPI_BOUNDARY_PREFIX}"
else
echo "Error: Failed to copy coder binary to ${CODER_NO_CAPS}. Boundary cannot be enabled." >&2
exit 1
fi
else
echo "Error: ENABLE_BOUNDARY=true, but 'coder' command not found. Boundary cannot be enabled." >&2
exit 1
fi
fi

export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
export AGENTAPI_ALLOWED_HOSTS="*"

export AGENTAPI_PID_FILE="${PID_FILE_PATH:-$module_path/agentapi.pid}"
# Only set state env vars when persistence is enabled and the binary supports
# it. State persistence requires agentapi >= v0.12.0.
Expand Down
9 changes: 9 additions & 0 deletions registry/coder/modules/agentapi/testdata/agentapi-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ for (const v of [
);
}
}
// Log boundary env vars.
for (const v of ["AGENTAPI_BOUNDARY_PREFIX"]) {
if (process.env[v]) {
fs.appendFileSync(
"/home/coder/agentapi-mock.log",
`\n${v}: ${process.env[v]}`,
);
}
}

// Write PID file for shutdown script.
if (process.env.AGENTAPI_PID_FILE) {
Expand Down
16 changes: 13 additions & 3 deletions registry/coder/modules/agentapi/testdata/agentapi-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ if [ -n "$AGENTAPI_CHAT_BASE_PATH" ]; then
export AGENTAPI_CHAT_BASE_PATH
fi

agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
bash -c aiagent \
> "$log_file_path" 2>&1
# Use boundary wrapper if configured by agentapi module.
# AGENTAPI_BOUNDARY_PREFIX is set by the agentapi module's main.sh
# and points to a wrapper script that runs the command through coder boundary.
if [ -n "${AGENTAPI_BOUNDARY_PREFIX:-}" ]; then
echo "Starting with boundary: ${AGENTAPI_BOUNDARY_PREFIX}" >> /home/coder/test-agentapi-start.log
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
"${AGENTAPI_BOUNDARY_PREFIX}" bash -c aiagent \
> "$log_file_path" 2>&1
else
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
bash -c aiagent \
> "$log_file_path" 2>&1
fi