diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index 33e582973..a2151f6cb 100644 --- a/registry/coder/modules/agentapi/README.md +++ b/registry/coder/modules/agentapi/README.md @@ -26,26 +26,8 @@ module "agentapi" { web_app_display_name = "Goose" cli_app_slug = "goose-cli" cli_app_display_name = "Goose CLI" - module_dir_name = local.module_dir_name + module_directory = local.module_directory install_agentapi = var.install_agentapi - pre_install_script = var.pre_install_script - post_install_script = var.post_install_script - start_script = local.start_script - 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_PROVIDER='${var.goose_provider}' \ - ARG_MODEL='${var.goose_model}' \ - ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \ - ARG_INSTALL='${var.install_goose}' \ - ARG_GOOSE_VERSION='${var.goose_version}' \ - /tmp/install.sh - EOT } ``` @@ -67,7 +49,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//` alongside other module files (e.g. `$HOME/.claude-module/agentapi-state.json`). +State and PID files are stored in the `module_directory` alongside other module files (e.g. `$HOME/.coder-modules/coder/claude-code/agentapi-state.json`). To enable: @@ -88,47 +70,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/agentapi.tftest.hcl b/registry/coder/modules/agentapi/agentapi.tftest.hcl index 87404c625..0179d426d 100644 --- a/registry/coder/modules/agentapi/agentapi.tftest.hcl +++ b/registry/coder/modules/agentapi/agentapi.tftest.hcl @@ -7,8 +7,6 @@ variables { web_app_slug = "test" cli_app_display_name = "Test CLI" cli_app_slug = "test-cli" - start_script = "echo test" - module_dir_name = ".test-module" } run "default_values" { @@ -51,11 +49,6 @@ run "default_values" { error_message = "shutdown script should contain ARG_PID_FILE_PATH" } - assert { - condition = can(regex("ARG_MODULE_DIR_NAME", coder_script.agentapi_shutdown.script)) - error_message = "shutdown script should contain ARG_MODULE_DIR_NAME" - } - assert { condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE", coder_script.agentapi_shutdown.script)) error_message = "shutdown script should contain ARG_ENABLE_STATE_PERSISTENCE" diff --git a/registry/coder/modules/agentapi/main.test.ts b/registry/coder/modules/agentapi/main.test.ts index 39d10ca7a..d529f85a4 100644 --- a/registry/coder/modules/agentapi/main.test.ts +++ b/registry/coder/modules/agentapi/main.test.ts @@ -44,7 +44,7 @@ interface SetupProps { moduleVariables?: Record; } -const moduleDirName = ".agentapi-module"; +const moduleDirectory = "/home/coder/.agentapi-module"; const setup = async (props?: SetupProps): Promise<{ id: string }> => { const projectDir = "/home/coder/project"; @@ -58,8 +58,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => { cli_app_display_name: "AgentAPI CLI", cli_app_slug: "agentapi-cli", agentapi_version: "latest", - module_dir_name: moduleDirName, - start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"), + module_directory: moduleDirectory, folder: projectDir, ...props?.moduleVariables, }, @@ -73,6 +72,19 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => { filePath: "/usr/bin/aiagent", content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"), }); + // Write the test start script directly to the module scripts dir, + // since start_script is no longer a Terraform variable. + const startScript = await loadTestFile(import.meta.dir, "agentapi-start.sh"); + await execContainer(id, [ + "bash", + "-c", + `mkdir -p ${moduleDirectory}/scripts`, + ]); + await writeExecutable({ + containerId: id, + filePath: `${moduleDirectory}/scripts/agentapi-start.sh`, + content: startScript, + }); return { id }; }; @@ -104,36 +116,6 @@ describe("agentapi", async () => { await expectAgentAPIStarted(id, 3827); }); - test("pre-post-install-scripts", async () => { - const { id } = await setup({ - moduleVariables: { - pre_install_script: `#!/bin/bash\necho "pre-install"`, - install_script: `#!/bin/bash\necho "install"`, - post_install_script: `#!/bin/bash\necho "post-install"`, - }, - }); - - await execModuleScript(id); - await expectAgentAPIStarted(id); - - const preInstallLog = await readFileContainer( - id, - `/home/coder/${moduleDirName}/pre_install.log`, - ); - const installLog = await readFileContainer( - id, - `/home/coder/${moduleDirName}/install.log`, - ); - const postInstallLog = await readFileContainer( - id, - `/home/coder/${moduleDirName}/post_install.log`, - ); - - expect(preInstallLog).toContain("pre-install"); - expect(installLog).toContain("install"); - expect(postInstallLog).toContain("post-install"); - }); - test("install-agentapi", async () => { const { id } = await setup({ skipAgentAPIMock: true }); @@ -313,10 +295,10 @@ describe("agentapi", async () => { "/home/coder/agentapi-mock.log", ); expect(mockLog).toContain( - `AGENTAPI_STATE_FILE: /home/coder/${moduleDirName}/agentapi-state.json`, + `AGENTAPI_STATE_FILE: ${moduleDirectory}/agentapi-state.json`, ); expect(mockLog).toContain( - `AGENTAPI_PID_FILE: /home/coder/${moduleDirName}/agentapi.pid`, + `AGENTAPI_PID_FILE: ${moduleDirectory}/agentapi.pid`, ); expect(mockLog).toContain("AGENTAPI_SAVE_STATE: true"); expect(mockLog).toContain("AGENTAPI_LOAD_STATE: true"); @@ -397,7 +379,7 @@ describe("agentapi", async () => { return await execContainer(containerId, [ "bash", "-c", - `ARG_TASK_ID=${taskId} ARG_AGENTAPI_PORT=3284 ARG_PID_FILE_PATH=${pidFilePath} ARG_ENABLE_STATE_PERSISTENCE=${enableStatePersistence} CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh`, + `ARG_TASK_ID=${taskId} ARG_AGENTAPI_PORT=3284 ARG_PID_FILE_PATH=${pidFilePath} ARG_ENABLE_STATE_PERSISTENCE=${enableStatePersistence} ARG_LIB_SCRIPT_PATH=/tmp/agentapi-lib.sh CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh`, ]); }; @@ -542,15 +524,15 @@ describe("agentapi", async () => { expect(result.stdout).toContain("Sending SIGTERM to AgentAPI"); }); - test("resolves default PID path from MODULE_DIR_NAME", async () => { + test("resolves default PID path from MODULE_DIRECTORY", async () => { const { id } = await setup({ moduleVariables: {}, skipAgentAPIMock: true, }); - // Start mock with PID file at the module_dir_name default location. - const defaultPidPath = `/home/coder/${moduleDirName}/agentapi.pid`; + // Start mock with PID file at the module_directory default location. + const defaultPidPath = `${moduleDirectory}/agentapi.pid`; await setupMocks(id, "normal", 204, defaultPidPath); - // Don't pass pidFilePath - let shutdown script compute it from MODULE_DIR_NAME. + // Don't pass pidFilePath - let shutdown script compute it from MODULE_DIRECTORY. const shutdownScript = await loadTestFile( import.meta.dir, "../scripts/agentapi-shutdown.sh", @@ -572,7 +554,7 @@ describe("agentapi", async () => { const result = await execContainer(id, [ "bash", "-c", - `ARG_TASK_ID=test-task ARG_AGENTAPI_PORT=3284 ARG_MODULE_DIR_NAME=${moduleDirName} ARG_ENABLE_STATE_PERSISTENCE=true CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh`, + `ARG_TASK_ID=test-task ARG_AGENTAPI_PORT=3284 ARG_MODULE_DIRECTORY=${moduleDirectory} ARG_ENABLE_STATE_PERSISTENCE=true ARG_LIB_SCRIPT_PATH=/tmp/agentapi-lib.sh CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh`, ]); expect(result.exitCode).toBe(0); @@ -586,7 +568,7 @@ describe("agentapi", async () => { skipAgentAPIMock: true, }); await setupMocks(id, "normal", 204); - // No pidFilePath and no MODULE_DIR_NAME, so no PID file can be resolved. + // No pidFilePath and no MODULE_DIRECTORY, so no PID file can be resolved. const result = await runShutdownScript(id, "test-task", "", "false"); expect(result.exitCode).toBe(0); @@ -613,109 +595,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 50d6bf685..5a878be06 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." @@ -165,41 +142,6 @@ variable "agentapi_subdomain" { } } -variable "module_dir_name" { - type = string - 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." @@ -208,21 +150,20 @@ variable "enable_state_persistence" { variable "state_file_path" { type = string - description = "Path to the AgentAPI state file. Defaults to $HOME//agentapi-state.json." + description = "Path to the AgentAPI state file. Defaults to /agentapi-state.json." default = "" } variable "pid_file_path" { type = string - description = "Path to the AgentAPI PID file. Defaults to $HOME//agentapi.pid." + description = "Path to the AgentAPI PID file. Defaults to /agentapi.pid." 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 +174,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 +185,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 +200,25 @@ resource "coder_script" "agentapi" { set -o errexit set -o pipefail - 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 + mkdir -p "${var.module_directory}" + echo -n '${base64encode(local.main_script)}' | base64 -d > "${local.main_script_destination}" + chmod +x "${local.main_script_destination}" + echo -n '${base64encode(local.lib_script)}' | base64 -d > "${local.lib_script_destination}" - ARG_MODULE_DIR_NAME='${var.module_dir_name}' \ + ARG_MODULE_DIRECTORY='${var.module_directory}' \ 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,17 +232,19 @@ resource "coder_script" "agentapi_shutdown" { #!/bin/bash set -o pipefail - echo -n '${base64encode(local.shutdown_script)}' | base64 -d > /tmp/agentapi-shutdown.sh - chmod +x /tmp/agentapi-shutdown.sh - echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.sh + mkdir -p "${var.module_directory}" + echo -n '${base64encode(local.shutdown_script)}' | base64 -d > "${local.shutdown_script_destination}" + chmod +x "${local.shutdown_script_destination}" + echo -n '${base64encode(local.lib_script)}' | base64 -d > "${local.lib_script_destination}" + ARG_MODULE_DIRECTORY='${var.module_directory}' \ ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \ ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \ ARG_AGENTAPI_PORT='${var.agentapi_port}' \ 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 8de176e44..9f7425a21 100644 --- a/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh +++ b/registry/coder/modules/agentapi/scripts/agentapi-shutdown.sh @@ -12,12 +12,13 @@ readonly TASK_ID="${ARG_TASK_ID:-}" readonly TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}" 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 MODULE_DIRECTORY="${ARG_MODULE_DIRECTORY:-}" +readonly PID_FILE_PATH="${ARG_PID_FILE_PATH:-${MODULE_DIRECTORY:+${MODULE_DIRECTORY}/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 d57f22614..000000000 --- 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 b0afa24af..7e91732cd 100644 --- a/registry/coder/modules/agentapi/scripts/main.sh +++ b/registry/coder/modules/agentapi/scripts/main.sh @@ -3,37 +3,29 @@ set -e set -x set -o nounset -MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME" +MODULE_DIRECTORY="$ARG_MODULE_DIRECTORY" WORKDIR="$ARG_WORKDIR" -PRE_INSTALL_SCRIPT="$ARG_PRE_INSTALL_SCRIPT" -INSTALL_SCRIPT="$ARG_INSTALL_SCRIPT" INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI" AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION" -START_SCRIPT="$ARG_START_SCRIPT" WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT" -POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT" 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 } -module_path="$HOME/${MODULE_DIR_NAME}" -mkdir -p "$module_path/scripts" +mkdir -p "${MODULE_DIRECTORY}/scripts" # Check for jq dependency if task log snapshot is enabled. if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then @@ -48,18 +40,6 @@ if [ ! -d "${WORKDIR}" ]; then mkdir -p "${WORKDIR}" echo "Folder created successfully." fi -if [ -n "${PRE_INSTALL_SCRIPT}" ]; then - echo "Running pre-install script..." - echo -n "${PRE_INSTALL_SCRIPT}" > "$module_path/pre_install.sh" - chmod +x "$module_path/pre_install.sh" - "$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log" -fi - -echo "Running install script..." -echo -n "${INSTALL_SCRIPT}" > "$module_path/install.sh" -chmod +x "$module_path/install.sh" -"$module_path/install.sh" 2>&1 | tee "$module_path/install.log" - # Install AgentAPI if enabled if [ "${INSTALL_AGENTAPI}" = "true" ]; then echo "Installing AgentAPI..." @@ -96,47 +76,30 @@ if ! command_exists agentapi; then exit 1 fi -echo -n "${START_SCRIPT}" > "$module_path/scripts/agentapi-start.sh" -echo -n "${WAIT_FOR_START_SCRIPT}" > "$module_path/scripts/agentapi-wait-for-start.sh" -chmod +x "$module_path/scripts/agentapi-start.sh" -chmod +x "$module_path/scripts/agentapi-wait-for-start.sh" - -if [ -n "${POST_INSTALL_SCRIPT}" ]; then - echo "Running post-install script..." - echo -n "${POST_INSTALL_SCRIPT}" > "$module_path/post_install.sh" - chmod +x "$module_path/post_install.sh" - "$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log" -fi +echo -n "${WAIT_FOR_START_SCRIPT}" > "${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" +chmod +x "${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" export LANG=en_US.UTF-8 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="*" -export AGENTAPI_PID_FILE="${PID_FILE_PATH:-$module_path/agentapi.pid}" +export AGENTAPI_PID_FILE="${PID_FILE_PATH:-${MODULE_DIRECTORY}/agentapi.pid}" # Only set state env vars when persistence is enabled and the binary supports # it. State persistence requires agentapi >= v0.12.0. if [ "${ENABLE_STATE_PERSISTENCE}" = "true" ]; then actual_version=$(agentapi_version) if version_at_least 0.12.0 "$actual_version"; then - export AGENTAPI_STATE_FILE="${STATE_FILE_PATH:-$module_path/agentapi-state.json}" + export AGENTAPI_STATE_FILE="${STATE_FILE_PATH:-${MODULE_DIRECTORY}/agentapi-state.json}" export AGENTAPI_SAVE_STATE="true" export AGENTAPI_LOAD_STATE="true" else echo "Warning: State persistence requires agentapi >= v0.12.0 (current: ${actual_version:-unknown}), skipping." fi fi -nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &> "$module_path/agentapi-start.log" & -"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}" +nohup "${MODULE_DIRECTORY}/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &> "${MODULE_DIRECTORY}/agentapi-start.log" & +"${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}" diff --git a/registry/coder/modules/agentapi/testdata/agentapi-mock.js b/registry/coder/modules/agentapi/testdata/agentapi-mock.js index e2e2d560d..d3eb4f8fd 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 417b64d09..01bfa8fc0 100644 --- a/registry/coder/modules/agentapi/testdata/agentapi-start.sh +++ b/registry/coder/modules/agentapi/testdata/agentapi-start.sh @@ -5,8 +5,8 @@ set -o pipefail use_prompt=${1:-false} port=${2:-3284} -module_path="$HOME/.agentapi-module" -log_file_path="$module_path/agentapi.log" +module_directory="$HOME/.agentapi-module" +log_file_path="$module_directory/agentapi.log" echo "using prompt: $use_prompt" >> /home/coder/test-agentapi-start.log echo "using port: $port" >> /home/coder/test-agentapi-start.log @@ -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