diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index 1514bb260..98f2a9ef5 100644 --- a/registry/coder/modules/agentapi/README.md +++ b/registry/coder/modules/agentapi/README.md @@ -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 = "1.0.2" + version = "1.1.0" agent_id = var.agent_id web_app_slug = local.app_slug diff --git a/registry/coder/modules/agentapi/main.test.ts b/registry/coder/modules/agentapi/main.test.ts index fab169679..773205cc1 100644 --- a/registry/coder/modules/agentapi/main.test.ts +++ b/registry/coder/modules/agentapi/main.test.ts @@ -148,4 +148,92 @@ describe("agentapi", async () => { ]); expect(respAgentAPI.exitCode).toBe(0); }); + + test("no-subdomain-base-path", async () => { + const { id } = await setup({ + moduleVariables: { + agentapi_subdomain: "false", + }, + }); + + const respModuleScript = await execModuleScript(id); + expect(respModuleScript.exitCode).toBe(0); + + await expectAgentAPIStarted(id); + const agentApiStartLog = await readFileContainer( + id, + "/home/coder/test-agentapi-start.log", + ); + expect(agentApiStartLog).toContain("Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat"); + }); + + test("validate-agentapi-version", async () => { + const cases = [ + { + moduleVariables: { + agentapi_version: "v0.3.2", + }, + shouldThrow: "", + }, + { + moduleVariables: { + agentapi_version: "v0.3.3", + }, + shouldThrow: "", + }, + { + moduleVariables: { + agentapi_version: "v0.0.1", + agentapi_subdomain: "false", + }, + shouldThrow: "Running with subdomain = false is only supported by agentapi >= v0.3.3.", + }, + { + moduleVariables: { + agentapi_version: "v0.3.2", + agentapi_subdomain: "false", + }, + shouldThrow: "Running with subdomain = false is only supported by agentapi >= v0.3.3.", + }, + { + moduleVariables: { + agentapi_version: "v0.3.3", + agentapi_subdomain: "false", + }, + shouldThrow: "", + }, + { + moduleVariables: { + agentapi_version: "v0.3.999", + agentapi_subdomain: "false", + }, + shouldThrow: "", + }, + { + moduleVariables: { + agentapi_version: "v0.999.999", + agentapi_subdomain: "false", + }, + }, + { + moduleVariables: { + agentapi_version: "v999.999.999", + agentapi_subdomain: "false", + }, + }, + { + moduleVariables: { + agentapi_version: "arbitrary-string-bypasses-validation", + }, + shouldThrow: "", + } + ]; + for (const { moduleVariables, shouldThrow } of cases) { + if (shouldThrow) { + expect(setup({ moduleVariables: moduleVariables as Record })).rejects.toThrow(shouldThrow); + } else { + expect(setup({ moduleVariables: moduleVariables as Record })).resolves.toBeDefined(); + } + } + }); }); diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf index 2e2c8669f..225a992d3 100644 --- a/registry/coder/modules/agentapi/main.tf +++ b/registry/coder/modules/agentapi/main.tf @@ -117,7 +117,7 @@ variable "install_agentapi" { variable "agentapi_version" { type = string description = "The version of AgentAPI to install." - default = "v0.2.3" + default = "v0.3.3" } variable "agentapi_port" { @@ -126,6 +126,31 @@ variable "agentapi_port" { default = 3284 } +locals { + # agentapi_subdomain_false_min_version_expr matches a semantic version >= v0.3.3. + # Initial support was added in v0.3.1 but configuration via environment variable + # was added in v0.3.3. + # This is unfortunately a regex because there is no builtin way to compare semantic versions in Terraform. + # See: https://regex101.com/r/oHPyRa/1 + agentapi_subdomain_false_min_version_expr = "^v(0\\.(3\\.[3-9]|3.[1-9]\\d+|[4-9]\\.\\d+|[1-9]\\d+\\.\\d+)|[1-9]\\d*\\.\\d+\\.\\d+)$" +} + +variable "agentapi_subdomain" { + type = bool + description = "Whether to use a subdomain for AgentAPI." + default = true + validation { + condition = var.agentapi_subdomain || ( + # If version doesn't look like a valid semantic version, just allow it. + # Note that boolean operators do not short-circuit in Terraform. + can(regex("^v\\d+\\.\\d+\\.\\d+$", var.agentapi_version)) ? + can(regex(local.agentapi_subdomain_false_min_version_expr, var.agentapi_version)) : + true + ) + error_message = "Running with subdomain = false is only supported by agentapi >= v0.3.3." + } +} + variable "module_dir_name" { type = string description = "Name of the subdirectory in the home directory for module files." @@ -140,7 +165,14 @@ locals { 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")) - main_script = file("${path.module}/scripts/main.sh") + // Chat base path is only set if not using a subdomain. + // NOTE: + // - Initial support for --chat-base-path was added in v0.3.1 but configuration + // via environment variable AGENTAPI_CHAT_BASE_PATH was added in v0.3.3. + // - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID + // for backward compatibility. + agentapi_chat_base_path = var.agentapi_subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${var.web_app_slug}/chat" + main_script = file("${path.module}/scripts/main.sh") } resource "coder_script" "agentapi" { @@ -165,6 +197,7 @@ resource "coder_script" "agentapi" { 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}' \ /tmp/main.sh EOT run_on_start = true @@ -178,7 +211,7 @@ resource "coder_app" "agentapi_web" { icon = var.web_app_icon order = var.web_app_order group = var.web_app_group - subdomain = true + subdomain = var.agentapi_subdomain healthcheck { url = "http://localhost:${var.agentapi_port}/status" interval = 3 diff --git a/registry/coder/modules/agentapi/scripts/main.sh b/registry/coder/modules/agentapi/scripts/main.sh index f7a5caab7..9cf7264be 100644 --- a/registry/coder/modules/agentapi/scripts/main.sh +++ b/registry/coder/modules/agentapi/scripts/main.sh @@ -13,6 +13,7 @@ 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:-}" set +o nounset command_exists() { @@ -92,5 +93,7 @@ export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 cd "${WORKDIR}" + +export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}" 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}" diff --git a/registry/coder/modules/agentapi/test-util.ts b/registry/coder/modules/agentapi/test-util.ts index 66860def5..a9062031e 100644 --- a/registry/coder/modules/agentapi/test-util.ts +++ b/registry/coder/modules/agentapi/test-util.ts @@ -24,7 +24,16 @@ export const setupContainer = async ({ }); const coderScript = findResourceInstance(state, "coder_script"); const id = await runContainer(image ?? "codercom/enterprise-node:latest"); - return { id, coderScript, cleanup: () => removeContainer(id) }; + return { + id, coderScript, cleanup: async () => { + if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1" || process.env["DEBUG"] === "yes") { + console.log(`Not removing container ${id} in debug mode`); + console.log(`Run "docker rm -f ${id}" to remove it manually.`); + } else { + await removeContainer(id); + } + } + }; }; export const loadTestFile = async ( diff --git a/registry/coder/modules/agentapi/testdata/agentapi-start.sh b/registry/coder/modules/agentapi/testdata/agentapi-start.sh index 1564fe032..739cb27d5 100644 --- a/registry/coder/modules/agentapi/testdata/agentapi-start.sh +++ b/registry/coder/modules/agentapi/testdata/agentapi-start.sh @@ -11,6 +11,12 @@ log_file_path="$module_path/agentapi.log" echo "using prompt: $use_prompt" >>/home/coder/test-agentapi-start.log echo "using port: $port" >>/home/coder/test-agentapi-start.log +AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}" +if [ -n "$AGENTAPI_CHAT_BASE_PATH" ]; then + echo "Using AGENTAPI_CHAT_BASE_PATH: $AGENTAPI_CHAT_BASE_PATH" >>/home/coder/test-agentapi-start.log + export AGENTAPI_CHAT_BASE_PATH +fi + agentapi server --port "$port" --term-width 67 --term-height 1190 -- \ bash -c aiagent \ >"$log_file_path" 2>&1