Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
0f5f04b
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 9, 2026
721c916
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 9, 2026
51952be
feat: centralised boundary install logic into agent api module
Mar 9, 2026
d5f5ec9
fix: update boundary_config back to boundary_config_path
Mar 9, 2026
d95064c
fi: remove accidental backup submodule
Mar 9, 2026
1b080c1
fix: shellcheck errors
Mar 9, 2026
077c33d
fix: add boundary subcommand validation
shanewhite97 Mar 10, 2026
080b805
fix: add boundary subcommand validation
shanewhite97 Mar 10, 2026
963af0b
fix: add boundary sub command validation
Mar 10, 2026
32c0d0e
chore: ran minor bump script
Mar 10, 2026
dd27fc2
docs: Tidy up variable comment
shanewhite97 Mar 10, 2026
ce7ce10
Merge branch 'main' into feat/agent-api-boundary-support
shanewhite97 Mar 10, 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
46 changes: 43 additions & 3 deletions registry/coder/modules/agentapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.2.0"
version = "2.3.0"

agent_id = var.agent_id
web_app_slug = local.app_slug
Expand Down Expand Up @@ -67,8 +67,7 @@ module "agentapi" {
AgentAPI can save and restore conversation state across workspace restarts.
This is disabled by default and requires agentapi binary >= v0.12.0.

State and PID files are stored in `$HOME/<module_dir_name>/` alongside other
module files (e.g. `$HOME/.claude-module/agentapi-state.json`).
State and PID files are stored in `$HOME/<module_dir_name>/` alongside other module files (e.g. `$HOME/.claude-module/agentapi-state.json`).

To enable:

Expand All @@ -89,6 +88,47 @@ 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
Comment thread
zedkipp marked this conversation as resolved.
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_path = "/home/coder/.config/coder_boundary/config.yaml"

# Optional: install boundary binary instead of using coder subcommand
# use_boundary_directly        = true
# boundary_version              = "0.6.0"
# compile_boundary_from_source  = false
}
```

### 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).
105 changes: 105 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,109 @@ describe("agentapi", async () => {
expect(result.stdout).toContain("Sending SIGTERM to AgentAPI");
});
});

describe("boundary", async () => {
test("boundary-disabled-by-default", async () => {
Comment thread
zedkipp marked this conversation as resolved.
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_path: "/tmp/test-boundary.yaml",
},
});
// Write boundary config to the path before running the module
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/test-boundary.yaml <<'EOF'
jail_type: landjail
proxy_port: 8087
log_level: warn
allowlist:
- "domain=api.example.com"
EOF`,
]);
// 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);
// Verify the config file exists at the specified path
const config = await readFileContainer(id, "/tmp/test-boundary.yaml");
expect(config).toContain("jail_type: landjail");
expect(config).toContain("proxy_port: 8087");
expect(config).toContain("domain=api.example.com");
// 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_path: "/tmp/test-boundary.yaml",
},
});
// Write boundary config
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/test-boundary.yaml <<'EOF'
jail_type: landjail
proxy_port: 8087
log_level: warn
EOF`,
]);
// 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");
});
});
});
45 changes: 45 additions & 0 deletions registry/coder/modules/agentapi/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,36 @@ 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_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. When compile_boundary_from_source is true, a valid git reference should be provided (tag, commit, branch)."
default = "latest"
}

variable "compile_boundary_from_source" {
type = bool
description = "Whether to compile boundary from source instead of using the official install script."
default = false
}

variable "use_boundary_directly" {
type = bool
description = "Whether to use boundary binary directly instead of coder boundary subcommand. When false (default), uses coder boundary subcommand. When true, installs and uses boundary binary from release."
default = false
}

variable "enable_state_persistence" {
type = bool
description = "Enable AgentAPI conversation state persistence across restarts."
Expand All @@ -182,6 +212,13 @@ variable "pid_file_path" {
default = ""
}

resource "coder_env" "boundary_config" {
count = var.enable_boundary && var.boundary_config_path != "" ? 1 : 0
agent_id = var.agent_id
name = "BOUNDARY_CONFIG"
value = var.boundary_config_path
}

locals {
# we always trim the slash for consistency
workdir = trimsuffix(var.folder, "/")
Expand All @@ -200,6 +237,7 @@ locals {
main_script = file("${path.module}/scripts/main.sh")
shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh")
lib_script = file("${path.module}/scripts/lib.sh")
boundary_script = file("${path.module}/scripts/boundary.sh")
}

resource "coder_script" "agentapi" {
Expand All @@ -214,6 +252,9 @@ resource "coder_script" "agentapi" {
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
chmod +x /tmp/main.sh
echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.sh

echo -n '${base64encode(local.boundary_script)}' | base64 -d > /tmp/agentapi-boundary.sh
chmod +x /tmp/agentapi-boundary.sh

ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
Expand All @@ -228,6 +269,10 @@ 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_VERSION='${var.boundary_version}' \
ARG_COMPILE_BOUNDARY_FROM_SOURCE='${var.compile_boundary_from_source}' \
ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \
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
95 changes: 95 additions & 0 deletions registry/coder/modules/agentapi/scripts/boundary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/bin/bash
# boundary.sh - Boundary installation and setup for agentapi module.
# Sourced by main.sh when ENABLE_BOUNDARY=true.
# Exports AGENTAPI_BOUNDARY_PREFIX for use by module start scripts.

validate_boundary_subcommand() {
if command_exists coder; then
if coder boundary --help > /dev/null 2>&1; then
return 0
else
echo "Error: 'coder' command found but does not support 'boundary' subcommand. Please enable install_boundary."
exit 1
fi
else
echo "Error: ENABLE_BOUNDARY=true, but 'coder' command not found. Boundary cannot be enabled." >&2
exit 1
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
Comment thread
shanewhite97 marked this conversation as resolved.
validate_boundary_subcommand
echo "Using coder boundary subcommand (provided by Coder)"
Comment thread
shanewhite97 marked this conversation as resolved.
fi
}

# Set up boundary: install, write config, create wrapper script.
# Exports AGENTAPI_BOUNDARY_PREFIX pointing to the wrapper script.
setup_boundary() {
local module_path="$1"

echo "Setting up coder boundary..."

# Install boundary binary if needed
install_boundary

# Determine which boundary command to use and create wrapper script
BOUNDARY_WRAPPER_SCRIPT="$module_path/boundary-wrapper.sh"

if [ "${COMPILE_BOUNDARY_FROM_SOURCE}" = "true" ] || [ "${USE_BOUNDARY_DIRECTLY}" = "true" ]; then
# Use boundary binary directly (from compilation or release installation)
cat > "${BOUNDARY_WRAPPER_SCRIPT}" << '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_path/coder-no-caps"
if ! cp "$(which 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_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
fi

chmod +x "${BOUNDARY_WRAPPER_SCRIPT}"
export AGENTAPI_BOUNDARY_PREFIX="${BOUNDARY_WRAPPER_SCRIPT}"
echo "Boundary wrapper configured: ${AGENTAPI_BOUNDARY_PREFIX}"
}
13 changes: 13 additions & 0 deletions registry/coder/modules/agentapi/scripts/main.sh
Comment thread
zedkipp marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ 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_VERSION="${ARG_BOUNDARY_VERSION:-latest}"
COMPILE_BOUNDARY_FROM_SOURCE="${ARG_COMPILE_BOUNDARY_FROM_SOURCE:-false}"
USE_BOUNDARY_DIRECTLY="${ARG_USE_BOUNDARY_DIRECTLY:-false}"
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 +113,18 @@ export LC_ALL=en_US.UTF-8

cd "${WORKDIR}"

# Set up boundary if enabled
export AGENTAPI_BOUNDARY_PREFIX=""
if [ "${ENABLE_BOUNDARY}" = "true" ]; then
# shellcheck source=boundary.sh
source /tmp/agentapi-boundary.sh
setup_boundary "$module_path"
fi
Comment thread
shanewhite97 marked this conversation as resolved.

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
Loading
Loading