diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index 33e58297..2221f835 100644 --- a/registry/coder/modules/agentapi/README.md +++ b/registry/coder/modules/agentapi/README.md @@ -88,47 +88,6 @@ 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_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). diff --git a/registry/coder/modules/agentapi/main.test.ts b/registry/coder/modules/agentapi/main.test.ts index 39d10ca7..cedf840c 100644 --- a/registry/coder/modules/agentapi/main.test.ts +++ b/registry/coder/modules/agentapi/main.test.ts @@ -613,109 +613,4 @@ 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_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"); - }); - }); }); diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf index 50d6bf68..53ca33de 100644 --- a/registry/coder/modules/agentapi/main.tf +++ b/registry/coder/modules/agentapi/main.tf @@ -93,29 +93,6 @@ variable "cli_app_slug" { description = "The slug of the CLI workspace app." } -variable "pre_install_script" { - type = string - description = "Custom script to run before installing the agent used by AgentAPI." - default = null -} - -variable "install_script" { - type = string - description = "Script to install the agent used by AgentAPI." - default = "" -} - -variable "post_install_script" { - type = string - description = "Custom script to run after installing the agent used by AgentAPI." - default = null -} - -variable "start_script" { - type = string - description = "Script that starts AgentAPI." -} - variable "install_agentapi" { type = bool description = "Whether to install AgentAPI." @@ -170,36 +147,6 @@ 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." @@ -218,11 +165,10 @@ 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 +variable "module_directory" { + type = string + description = "" + default = "$HOME/.coder-modules/coder/agentapi" } locals { @@ -233,10 +179,6 @@ locals { # we always trim the slash for consistency workdir = trimsuffix(var.folder, "/") - encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : "" - encoded_install_script = var.install_script != null ? base64encode(var.install_script) : "" - encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : "" - agentapi_start_script_b64 = base64encode(var.start_script) agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh")) // Chat base path is only set if not using a subdomain. // NOTE: @@ -248,7 +190,10 @@ 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") + + main_script_destination = "${var.module_directory}/main.sh" + lib_script_destination = "${var.module_directory}/agentapi-lib.sh" + shutdown_script_destination = "${var.module_directory}/agentapi-shutdown.sh" } resource "coder_script" "agentapi" { @@ -260,34 +205,24 @@ resource "coder_script" "agentapi" { set -o errexit set -o pipefail - echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh + echo -n '${base64encode(local.main_script)}' | base64 -d > "${local.main_script_destination}" 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 + echo -n '${base64encode(local.lib_script)}' | base64 -d > "${local.lib_script_destination}" ARG_MODULE_DIR_NAME='${var.module_dir_name}' \ ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \ - ARG_PRE_INSTALL_SCRIPT="$(echo -n '${local.encoded_pre_install_script}' | base64 -d)" \ - ARG_INSTALL_SCRIPT="$(echo -n '${local.encoded_install_script}' | base64 -d)" \ ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \ ARG_AGENTAPI_VERSION='${var.agentapi_version}' \ - ARG_START_SCRIPT="$(echo -n '${local.agentapi_start_script_b64}' | base64 -d)" \ ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \ - ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \ ARG_AGENTAPI_PORT='${var.agentapi_port}' \ 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}' \ - /tmp/main.sh + ARG_LIB_SCRIPT_PATH='${local.lib_script_destination}' \ + "${local.main_script_destination}" EOT run_on_start = true } @@ -301,9 +236,9 @@ resource "coder_script" "agentapi_shutdown" { #!/bin/bash set -o pipefail - echo -n '${base64encode(local.shutdown_script)}' | base64 -d > /tmp/agentapi-shutdown.sh + echo -n '${base64encode(local.shutdown_script)}' | base64 -d > "${local.shutdown_script_destination}" chmod +x /tmp/agentapi-shutdown.sh - echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.sh + echo -n '${base64encode(local.lib_script)}' | base64 -d > "${local.lib_script_destination}" ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \ ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \ @@ -311,7 +246,8 @@ resource "coder_script" "agentapi_shutdown" { ARG_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \ ARG_MODULE_DIR_NAME='${var.module_dir_name}' \ ARG_PID_FILE_PATH='${var.pid_file_path}' \ - /tmp/agentapi-shutdown.sh + ARG_LIB_SCRIPT_PATH='${local.lib_script_destination}' \ + "${local.shutdown_script_destination}" EOT } diff --git a/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh b/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh index 8de176e4..09ddfc87 100644 --- a/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh +++ b/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh @@ -14,10 +14,11 @@ readonly AGENTAPI_PORT="${ARG_AGENTAPI_PORT:-3284}" readonly ENABLE_STATE_PERSISTENCE="${ARG_ENABLE_STATE_PERSISTENCE:-false}" readonly MODULE_DIR_NAME="${ARG_MODULE_DIR_NAME:-}" readonly PID_FILE_PATH="${ARG_PID_FILE_PATH:-${MODULE_DIR_NAME:+$HOME/$MODULE_DIR_NAME/agentapi.pid}}" +readonly LIB_SCRIPT_PATH="${ARG_LIB_SCRIPT_PATH}" # Source shared utilities (written by the coder_script wrapper). # shellcheck source=lib.sh -source /tmp/agentapi-lib.sh +source "${LIB_SCRIPT_PATH}" # Runtime environment variables. readonly CODER_AGENT_URL="${CODER_AGENT_URL:-}" diff --git a/registry/coder/modules/agentapi/scripts/boundary.sh b/registry/coder/modules/agentapi/scripts/boundary.sh deleted file mode 100644 index d57f2261..00000000 --- a/registry/coder/modules/agentapi/scripts/boundary.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/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 - validate_boundary_subcommand - echo "Using coder boundary subcommand (provided by Coder)" - 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}" -} diff --git a/registry/coder/modules/agentapi/scripts/main.sh b/registry/coder/modules/agentapi/scripts/main.sh index b0afa24a..09cdb288 100644 --- a/registry/coder/modules/agentapi/scripts/main.sh +++ b/registry/coder/modules/agentapi/scripts/main.sh @@ -16,17 +16,14 @@ 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:-}" +LIB_SCRIPT_PATH="$ARG_LIB_SCRIPT_PATH" set +o nounset # shellcheck source=lib.sh -source /tmp/agentapi-lib.sh +source "${LIB_SCRIPT_PATH}" command_exists() { command -v "$1" > /dev/null 2>&1 @@ -113,14 +110,6 @@ 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 - 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="*" diff --git a/registry/coder/modules/agentapi/testdata/agentapi-mock.js b/registry/coder/modules/agentapi/testdata/agentapi-mock.js index e2e2d560..d3eb4f8f 100644 --- a/registry/coder/modules/agentapi/testdata/agentapi-mock.js +++ b/registry/coder/modules/agentapi/testdata/agentapi-mock.js @@ -31,16 +31,6 @@ 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) { const path = require("path"); diff --git a/registry/coder/modules/agentapi/testdata/agentapi-start.sh b/registry/coder/modules/agentapi/testdata/agentapi-start.sh index 417b64d0..259eb0c9 100644 --- a/registry/coder/modules/agentapi/testdata/agentapi-start.sh +++ b/registry/coder/modules/agentapi/testdata/agentapi-start.sh @@ -17,16 +17,6 @@ if [ -n "$AGENTAPI_CHAT_BASE_PATH" ]; then export AGENTAPI_CHAT_BASE_PATH fi -# 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 +agentapi server --port "$port" --term-width 67 --term-height 1190 -- \ + bash -c aiagent \ + > "$log_file_path" 2>&1