Skip to content

Commit 2ee14fd

Browse files
shanewhite97Shane WhiteCopilot35C4n0r
authored
feat: provide boundary support for agent modules (#780)
## Description Enable any agent module to run its AI agent inside Coder's Agent Boundaries. The agentapi module handles boundary installation, config setup, and wrapper script creation, then exports AGENTAPI_BOUNDARY_PREFIX for consuming modules to use in their start scripts. Supports three boundary installation modes: - coder boundary subcommand (default, Coder v2.30+) - Standalone binary via install script (use_boundary_directly) - Compiled from source (compile_boundary_from_source) Users must provide a boundary config.yaml with their allowlist and settings when enabling boundary. Closes #457 ## Type of Change - [x] Feature/enhancement ## Module Information **Path:** `registry/coder/modules/agentapi` **Breaking change:** No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally --------- Co-authored-by: Shane White <shane.white@cloudsecure.ltd> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com>
1 parent 183bd57 commit 2ee14fd

7 files changed

Lines changed: 323 additions & 6 deletions

File tree

registry/coder/modules/agentapi/README.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
1616
```tf
1717
module "agentapi" {
1818
source = "registry.coder.com/coder/agentapi/coder"
19-
version = "2.2.0"
19+
version = "2.3.0"
2020
2121
agent_id = var.agent_id
2222
web_app_slug = local.app_slug
@@ -67,8 +67,7 @@ module "agentapi" {
6767
AgentAPI can save and restore conversation state across workspace restarts.
6868
This is disabled by default and requires agentapi binary >= v0.12.0.
6969

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

7372
To enable:
7473

@@ -89,6 +88,47 @@ module "agentapi" {
8988
}
9089
```
9190

91+
## Boundary (Network Filtering)
92+
93+
The agentapi module supports optional [Agent Boundaries](https://coder.com/docs/ai-coder/agent-boundaries)
94+
for network filtering. When enabled, the module sets up a `AGENTAPI_BOUNDARY_PREFIX` environment
95+
variable that points to a wrapper script. Agent modules should use this prefix in their
96+
start scripts to run the agent process through boundary.
97+
98+
Boundary requires a `config.yaml` file with your allowlist, jail type, proxy port, and log
99+
level. See the [Agent Boundaries documentation](https://coder.com/docs/ai-coder/agent-boundaries)
100+
for configuration details.
101+
To enable:
102+
103+
```tf
104+
module "agentapi" {
105+
# ... other config
106+
enable_boundary = true
107+
boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml"
108+
109+
# Optional: install boundary binary instead of using coder subcommand
110+
# use_boundary_directly        = true
111+
# boundary_version              = "0.6.0"
112+
# compile_boundary_from_source  = false
113+
}
114+
```
115+
116+
### Contract for agent modules
117+
118+
When `enable_boundary = true`, the agentapi module exports `AGENTAPI_BOUNDARY_PREFIX`
119+
as an environment variable pointing to a wrapper script. Agent module start scripts
120+
should check for this variable and use it to prefix the agent command:
121+
122+
```bash
123+
if [ -n "${AGENTAPI_BOUNDARY_PREFIX:-}" ]; then
124+
agentapi server -- "${AGENTAPI_BOUNDARY_PREFIX}" my-agent "${ARGS[@]}" &
125+
else
126+
agentapi server -- my-agent "${ARGS[@]}" &
127+
fi
128+
```
129+
130+
This ensures only the agent process is sandboxed while agentapi itself runs unrestricted.
131+
92132
## For module developers
93133

94134
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).

registry/coder/modules/agentapi/main.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,109 @@ describe("agentapi", async () => {
613613
expect(result.stdout).toContain("Sending SIGTERM to AgentAPI");
614614
});
615615
});
616+
617+
describe("boundary", async () => {
618+
test("boundary-disabled-by-default", async () => {
619+
const { id } = await setup();
620+
await execModuleScript(id);
621+
await expectAgentAPIStarted(id);
622+
// Config file should NOT exist when boundary is disabled
623+
const configCheck = await execContainer(id, [
624+
"bash",
625+
"-c",
626+
"test -f /home/coder/.config/coder_boundary/config.yaml && echo exists || echo missing",
627+
]);
628+
expect(configCheck.stdout.trim()).toBe("missing");
629+
// AGENTAPI_BOUNDARY_PREFIX should NOT be in the mock log
630+
const mockLog = await readFileContainer(
631+
id,
632+
"/home/coder/agentapi-mock.log",
633+
);
634+
expect(mockLog).not.toContain("AGENTAPI_BOUNDARY_PREFIX:");
635+
});
636+
637+
test("boundary-enabled", async () => {
638+
const { id } = await setup({
639+
moduleVariables: {
640+
enable_boundary: "true",
641+
boundary_config_path: "/tmp/test-boundary.yaml",
642+
},
643+
});
644+
// Write boundary config to the path before running the module
645+
await execContainer(id, [
646+
"bash",
647+
"-c",
648+
`cat > /tmp/test-boundary.yaml <<'EOF'
649+
jail_type: landjail
650+
proxy_port: 8087
651+
log_level: warn
652+
allowlist:
653+
- "domain=api.example.com"
654+
EOF`,
655+
]);
656+
// Add mock coder binary for boundary setup
657+
await writeExecutable({
658+
containerId: id,
659+
filePath: "/usr/bin/coder",
660+
content: `#!/bin/bash
661+
if [ "$1" = "boundary" ]; then
662+
shift; shift; exec "$@"
663+
fi
664+
echo "mock coder"`,
665+
});
666+
await execModuleScript(id);
667+
await expectAgentAPIStarted(id);
668+
// Verify the config file exists at the specified path
669+
const config = await readFileContainer(id, "/tmp/test-boundary.yaml");
670+
expect(config).toContain("jail_type: landjail");
671+
expect(config).toContain("proxy_port: 8087");
672+
expect(config).toContain("domain=api.example.com");
673+
// AGENTAPI_BOUNDARY_PREFIX should be exported
674+
const mockLog = await readFileContainer(
675+
id,
676+
"/home/coder/agentapi-mock.log",
677+
);
678+
expect(mockLog).toContain("AGENTAPI_BOUNDARY_PREFIX:");
679+
// E2E: start script should have used the wrapper
680+
const startLog = await readFileContainer(
681+
id,
682+
"/home/coder/test-agentapi-start.log",
683+
);
684+
expect(startLog).toContain("Starting with boundary:");
685+
});
686+
687+
test("boundary-enabled-no-coder-binary", async () => {
688+
const { id } = await setup({
689+
moduleVariables: {
690+
enable_boundary: "true",
691+
boundary_config_path: "/tmp/test-boundary.yaml",
692+
},
693+
});
694+
// Write boundary config
695+
await execContainer(id, [
696+
"bash",
697+
"-c",
698+
`cat > /tmp/test-boundary.yaml <<'EOF'
699+
jail_type: landjail
700+
proxy_port: 8087
701+
log_level: warn
702+
EOF`,
703+
]);
704+
// Remove coder binary to simulate it not being available
705+
await execContainer(
706+
id,
707+
[
708+
"bash",
709+
"-c",
710+
"rm -f /usr/bin/coder /usr/local/bin/coder 2>/dev/null; hash -r",
711+
],
712+
["--user", "root"],
713+
);
714+
const resp = await execModuleScript(id);
715+
// Script should fail because coder binary is required
716+
expect(resp.exitCode).not.toBe(0);
717+
const scriptLog = await readFileContainer(id, "/home/coder/script.log");
718+
expect(scriptLog).toContain("Boundary cannot be enabled");
719+
});
720+
});
616721
});

registry/coder/modules/agentapi/main.tf

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,36 @@ variable "module_dir_name" {
164164
description = "Name of the subdirectory in the home directory for module files."
165165
}
166166

167+
variable "enable_boundary" {
168+
type = bool
169+
description = "Enable coder boundary for network filtering. Requires boundary_config to be set."
170+
default = false
171+
}
172+
173+
variable "boundary_config_path" {
174+
type = string
175+
description = "Path to boundary config.yaml inside the workspace. If provided, exposed as BOUNDARY_CONFIG env var."
176+
default = ""
177+
}
178+
179+
variable "boundary_version" {
180+
type = string
181+
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)."
182+
default = "latest"
183+
}
184+
185+
variable "compile_boundary_from_source" {
186+
type = bool
187+
description = "Whether to compile boundary from source instead of using the official install script."
188+
default = false
189+
}
190+
191+
variable "use_boundary_directly" {
192+
type = bool
193+
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."
194+
default = false
195+
}
196+
167197
variable "enable_state_persistence" {
168198
type = bool
169199
description = "Enable AgentAPI conversation state persistence across restarts."
@@ -182,6 +212,13 @@ variable "pid_file_path" {
182212
default = ""
183213
}
184214

215+
resource "coder_env" "boundary_config" {
216+
count = var.enable_boundary && var.boundary_config_path != "" ? 1 : 0
217+
agent_id = var.agent_id
218+
name = "BOUNDARY_CONFIG"
219+
value = var.boundary_config_path
220+
}
221+
185222
locals {
186223
# we always trim the slash for consistency
187224
workdir = trimsuffix(var.folder, "/")
@@ -200,6 +237,7 @@ locals {
200237
main_script = file("${path.module}/scripts/main.sh")
201238
shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh")
202239
lib_script = file("${path.module}/scripts/lib.sh")
240+
boundary_script = file("${path.module}/scripts/boundary.sh")
203241
}
204242

205243
resource "coder_script" "agentapi" {
@@ -214,6 +252,9 @@ resource "coder_script" "agentapi" {
214252
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
215253
chmod +x /tmp/main.sh
216254
echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.sh
255+
256+
echo -n '${base64encode(local.boundary_script)}' | base64 -d > /tmp/agentapi-boundary.sh
257+
chmod +x /tmp/agentapi-boundary.sh
217258
218259
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
219260
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
@@ -228,6 +269,10 @@ resource "coder_script" "agentapi" {
228269
ARG_AGENTAPI_CHAT_BASE_PATH='${local.agentapi_chat_base_path}' \
229270
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
230271
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \
272+
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
273+
ARG_BOUNDARY_VERSION='${var.boundary_version}' \
274+
ARG_COMPILE_BOUNDARY_FROM_SOURCE='${var.compile_boundary_from_source}' \
275+
ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \
231276
ARG_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \
232277
ARG_STATE_FILE_PATH='${var.state_file_path}' \
233278
ARG_PID_FILE_PATH='${var.pid_file_path}' \
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/bin/bash
2+
# boundary.sh - Boundary installation and setup for agentapi module.
3+
# Sourced by main.sh when ENABLE_BOUNDARY=true.
4+
# Exports AGENTAPI_BOUNDARY_PREFIX for use by module start scripts.
5+
6+
validate_boundary_subcommand() {
7+
if command_exists coder; then
8+
if coder boundary --help > /dev/null 2>&1; then
9+
return 0
10+
else
11+
echo "Error: 'coder' command found but does not support 'boundary' subcommand. Please enable install_boundary."
12+
exit 1
13+
fi
14+
else
15+
echo "Error: ENABLE_BOUNDARY=true, but 'coder' command not found. Boundary cannot be enabled." >&2
16+
exit 1
17+
fi
18+
}
19+
20+
# Install boundary binary if needed.
21+
# Uses one of three strategies:
22+
# 1. Compile from source (compile_boundary_from_source=true)
23+
# 2. Install from release (use_boundary_directly=true)
24+
# 3. Use coder boundary subcommand (default, no installation needed)
25+
install_boundary() {
26+
if [ "${COMPILE_BOUNDARY_FROM_SOURCE}" = "true" ]; then
27+
echo "Compiling boundary from source (version: ${BOUNDARY_VERSION})"
28+
29+
# Remove existing boundary directory to allow re-running safely
30+
if [ -d boundary ]; then
31+
rm -rf boundary
32+
fi
33+
34+
echo "Cloning boundary repository"
35+
git clone https://github.com/coder/boundary.git
36+
cd boundary || exit 1
37+
git checkout "${BOUNDARY_VERSION}"
38+
39+
make build
40+
41+
sudo cp boundary /usr/local/bin/
42+
sudo chmod +x /usr/local/bin/boundary
43+
cd - || exit 1
44+
elif [ "${USE_BOUNDARY_DIRECTLY}" = "true" ]; then
45+
echo "Installing boundary using official install script (version: ${BOUNDARY_VERSION})"
46+
curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "${BOUNDARY_VERSION}"
47+
else
48+
validate_boundary_subcommand
49+
echo "Using coder boundary subcommand (provided by Coder)"
50+
fi
51+
}
52+
53+
# Set up boundary: install, write config, create wrapper script.
54+
# Exports AGENTAPI_BOUNDARY_PREFIX pointing to the wrapper script.
55+
setup_boundary() {
56+
local module_path="$1"
57+
58+
echo "Setting up coder boundary..."
59+
60+
# Install boundary binary if needed
61+
install_boundary
62+
63+
# Determine which boundary command to use and create wrapper script
64+
BOUNDARY_WRAPPER_SCRIPT="$module_path/boundary-wrapper.sh"
65+
66+
if [ "${COMPILE_BOUNDARY_FROM_SOURCE}" = "true" ] || [ "${USE_BOUNDARY_DIRECTLY}" = "true" ]; then
67+
# Use boundary binary directly (from compilation or release installation)
68+
cat > "${BOUNDARY_WRAPPER_SCRIPT}" << 'WRAPPER_EOF'
69+
#!/usr/bin/env bash
70+
set -euo pipefail
71+
exec boundary -- "$@"
72+
WRAPPER_EOF
73+
else
74+
# Use coder boundary subcommand (default)
75+
# Copy coder binary to strip CAP_NET_ADMIN capabilities.
76+
# This is necessary because boundary doesn't work with privileged binaries
77+
# (you can't launch privileged binaries inside network namespaces unless
78+
# you have sys_admin).
79+
CODER_NO_CAPS="$module_path/coder-no-caps"
80+
if ! cp "$(which coder)" "$CODER_NO_CAPS"; then
81+
echo "Error: Failed to copy coder binary to ${CODER_NO_CAPS}. Boundary cannot be enabled." >&2
82+
exit 1
83+
fi
84+
cat > "${BOUNDARY_WRAPPER_SCRIPT}" << 'WRAPPER_EOF'
85+
#!/usr/bin/env bash
86+
set -euo pipefail
87+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
88+
exec "${SCRIPT_DIR}/coder-no-caps" boundary -- "$@"
89+
WRAPPER_EOF
90+
fi
91+
92+
chmod +x "${BOUNDARY_WRAPPER_SCRIPT}"
93+
export AGENTAPI_BOUNDARY_PREFIX="${BOUNDARY_WRAPPER_SCRIPT}"
94+
echo "Boundary wrapper configured: ${AGENTAPI_BOUNDARY_PREFIX}"
95+
}

registry/coder/modules/agentapi/scripts/main.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
1616
AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}"
1717
TASK_ID="${ARG_TASK_ID:-}"
1818
TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}"
19+
ENABLE_BOUNDARY="${ARG_ENABLE_BOUNDARY:-false}"
20+
BOUNDARY_VERSION="${ARG_BOUNDARY_VERSION:-latest}"
21+
COMPILE_BOUNDARY_FROM_SOURCE="${ARG_COMPILE_BOUNDARY_FROM_SOURCE:-false}"
22+
USE_BOUNDARY_DIRECTLY="${ARG_USE_BOUNDARY_DIRECTLY:-false}"
1923
ENABLE_STATE_PERSISTENCE="${ARG_ENABLE_STATE_PERSISTENCE:-false}"
2024
STATE_FILE_PATH="${ARG_STATE_FILE_PATH:-}"
2125
PID_FILE_PATH="${ARG_PID_FILE_PATH:-}"
@@ -109,9 +113,18 @@ export LC_ALL=en_US.UTF-8
109113

110114
cd "${WORKDIR}"
111115

116+
# Set up boundary if enabled
117+
export AGENTAPI_BOUNDARY_PREFIX=""
118+
if [ "${ENABLE_BOUNDARY}" = "true" ]; then
119+
# shellcheck source=boundary.sh
120+
source /tmp/agentapi-boundary.sh
121+
setup_boundary "$module_path"
122+
fi
123+
112124
export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
113125
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
114126
export AGENTAPI_ALLOWED_HOSTS="*"
127+
115128
export AGENTAPI_PID_FILE="${PID_FILE_PATH:-$module_path/agentapi.pid}"
116129
# Only set state env vars when persistence is enabled and the binary supports
117130
# it. State persistence requires agentapi >= v0.12.0.

registry/coder/modules/agentapi/testdata/agentapi-mock.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ for (const v of [
3131
);
3232
}
3333
}
34+
// Log boundary env vars.
35+
for (const v of ["AGENTAPI_BOUNDARY_PREFIX"]) {
36+
if (process.env[v]) {
37+
fs.appendFileSync(
38+
"/home/coder/agentapi-mock.log",
39+
`\n${v}: ${process.env[v]}`,
40+
);
41+
}
42+
}
3443

3544
// Write PID file for shutdown script.
3645
if (process.env.AGENTAPI_PID_FILE) {

0 commit comments

Comments
 (0)