Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6e18248
feat(coder/modules/agentapi): enhance start script and configuration …
35C4n0r Feb 3, 2026
3f68310
feat(coder/modules/agentapi): enhance start script and configuration …
35C4n0r Feb 4, 2026
f93b366
fix: change agentapi source
35C4n0r Feb 4, 2026
787ce3f
fix: add start.sh log to the agentapi-start.logs too.
35C4n0r Feb 4, 2026
6db604b
chore: bun fmt
35C4n0r Feb 4, 2026
833f0ac
chore: fix tests
35C4n0r Feb 4, 2026
b580ec2
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv
35C4n0r Feb 4, 2026
3d4d24b
chore: revert claude changes
35C4n0r Feb 4, 2026
ac1fb95
chore: improve doc
35C4n0r Feb 4, 2026
63c5e2c
chore: improve doc
35C4n0r Feb 4, 2026
a332181
chore: remove comments
35C4n0r Feb 4, 2026
f52ef07
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv
35C4n0r Feb 4, 2026
80f47d0
chore: bump module versions (major)
35C4n0r Feb 4, 2026
4459d39
feat: remove the responsibility of running install and start script f…
35C4n0r Feb 5, 2026
d635583
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv
35C4n0r Feb 5, 2026
15ca0aa
feat: sync main.sh
35C4n0r Feb 6, 2026
c630722
fix: fix typo
35C4n0r Feb 6, 2026
5edf4a9
fix: complete commands
35C4n0r Feb 6, 2026
85e7da3
fix: complete commands
35C4n0r Feb 6, 2026
03ac608
feat: overwrite agentapi logs instead of appending them
35C4n0r Feb 6, 2026
df2d72f
feat: add mock coder since we now depend on coder command to sync scr…
35C4n0r Feb 6, 2026
65c40ed
feat: rector agentapi_server_type to agent_name
35C4n0r Feb 6, 2026
c079545
chore: fix tests
35C4n0r Feb 6, 2026
4bbc6d9
feat: move install and start script logic to agentapi via agent-helper
35C4n0r Feb 6, 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
47 changes: 25 additions & 22 deletions registry/coder/modules/agentapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,21 @@ 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.1.0"
version = "4.0.0"

agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = "Goose"
cli_app_slug = "goose-cli"
cli_app_display_name = "Goose CLI"
web_app_display_name = "ClaudeCode"
cli_app_slug = "claude-cli"
cli_app_display_name = "Claude CLI"
module_dir_name = local.module_dir_name
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
agentapi_server_type = "claude"
agentapi_term_width = 67
agentapi_term_height = 1190
}
```

Expand All @@ -65,3 +50,21 @@ module "agentapi" {
## 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).

### agent-command.sh

The calling module must create an executable script at `$HOME/{module_dir_name}/agent-command.sh` before this module's script runs. This script should contain the command to start your AI agent.

Example:

```bash
#!/bin/bash
module_path="$HOME/.my-module"

cat > "$module_path/agent-command.sh" << EOF
#!/bin/bash
my-agent-command --my-agent-flags
EOF
```

The AgentAPI module will run this script with the agentapi server.
121 changes: 85 additions & 36 deletions registry/coder/modules/agentapi/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
setDefaultTimeout,
beforeAll,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
execContainer,
readFileContainer,
runTerraformInit,
runTerraformApply,
} from "~test";
import {
loadTestFile,
writeExecutable,
Expand Down Expand Up @@ -58,21 +63,37 @@
cli_app_display_name: "AgentAPI CLI",
cli_app_slug: "agentapi-cli",
agentapi_version: "latest",
agent_name: "claude",
module_dir_name: moduleDirName,
start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"),
folder: projectDir,
pre_install_script: "echo 'Pre-install'",
install_script: "echo 'Install'",
post_install_script: "echo 'Post-install'",
start_script: "echo 'Start'",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
moduleDir: import.meta.dir,
});
// Create the ai agent mock binary
await writeExecutable({
containerId: id,
filePath: "/usr/bin/aiagent",
content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"),
});
// Create the agent-command.sh script that the module expects
await execContainer(id, [
"bash",
"-c",
`mkdir -p /home/coder/${moduleDirName}`,
]);
await writeExecutable({
containerId: id,
filePath: `/home/coder/${moduleDirName}/agent-command.sh`,
content: "#!/bin/bash\nexec aiagent",
});
return { id };
};

Expand Down Expand Up @@ -104,36 +125,6 @@
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 });

Expand All @@ -160,12 +151,12 @@
expect(respModuleScript.exitCode).toBe(0);

await expectAgentAPIStarted(id);
const agentApiStartLog = await readFileContainer(
const agentApiMockLog = await readFileContainer(
id,
"/home/coder/test-agentapi-start.log",
"/home/coder/agentapi-mock.log",
);
expect(agentApiStartLog).toContain(
"Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat",
expect(agentApiMockLog).toContain(
"AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat",
);
});

Expand Down Expand Up @@ -258,6 +249,64 @@
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
});

test("enable-agentapi-false", async () => {
// Test that when enable_agentapi is false:
// 1. AgentAPI web app is not created
// 2. AgentAPI is not started
// 3. CLI app still works and uses agent-command.sh
const { id } = await setup({
moduleVariables: {
enable_agentapi: "false",
cli_app: "true",
},
});

const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);

// Verify agentapi is not running on the default port
const respCheck = await execContainer(id, [
"bash",
"-c",
"curl -fs -o /dev/null http://localhost:3284/status || echo 'not running'",
]);
expect(respCheck.stdout).toContain("not running");

// Verify agent-command.sh script exists and is executable
const respAgentCommand = await execContainer(id, [
"bash",
"-c",
`test -x /home/coder/${moduleDirName}/agent-command.sh && echo 'exists'`,
]);
expect(respAgentCommand.stdout).toContain("exists");
});

test("task-app-id-output", async () => {
// Test that task_app_id output is null when enable_agentapi is false
const projectDir = "/home/coder/project";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
experiment_report_tasks: "true",
install_agentapi: "false",
web_app_display_name: "AgentAPI Web",
web_app_slug: "agentapi-web",
web_app_icon: "/icon/coder.svg",
cli_app_display_name: "AgentAPI CLI",
cli_app_slug: "agentapi-cli",
agentapi_version: "latest",
agent_name: "claude",
module_dir_name: moduleDirName,
folder: projectDir,
pre_install_script: "echo 'Pre-install'",
install_script: "echo 'Install'",
post_install_script: "echo 'Post-install'",
start_script: "echo 'Start'",
enable_agentapi: "false",
});

expect(state.outputs.task_app_id.value).toBeNull();

Check failure on line 307 in registry/coder/modules/agentapi/main.test.ts

View workflow job for this annotation

GitHub Actions / Validate Terraform output

TypeError: undefined is not an object (evaluating 'state.outputs.task_app_id.value')

at <anonymous> (/home/runner/work/registry/registry/registry/coder/modules/agentapi/main.test.ts:307:26)
});

describe("shutdown script", async () => {
const setupMocks = async (
containerId: string,
Expand Down
75 changes: 64 additions & 11 deletions registry/coder/modules/agentapi/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ variable "start_script" {
description = "Script that starts AgentAPI."
}

variable "enable_agentapi" {
type = bool
description = "Whether to enable AgentAPI. If false, AgentAPI will not be installed or started, and the web app will not be created."
default = true
}

variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
Expand All @@ -128,6 +134,29 @@ variable "agentapi_port" {
default = 3284
}

variable "agent_name" {
type = string
description = "The agent's name. This is used as server type for AgentAPI, passed using --agent flag."
}

variable "agentapi_term_width" {
type = number
description = "The terminal width for AgentAPI."
default = 67
}

variable "agentapi_term_height" {
type = number
description = "The terminal height for AgentAPI."
default = 1190
}

variable "agentapi_initial_prompt" {
type = string
description = "Initial prompt for the agent. Recommended only if the agent doesn't support initial prompt in interaction mode."
default = null
}

variable "task_log_snapshot" {
type = bool
description = "Capture last 10 messages when workspace stops for offline viewing while task is paused."
Expand Down Expand Up @@ -168,10 +197,7 @@ variable "module_dir_name" {
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)
encoded_initial_prompt = var.agentapi_initial_prompt != null ? base64encode(var.agentapi_initial_prompt) : ""
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:
Expand All @@ -182,30 +208,51 @@ locals {
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")
shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh")

start_script_name = "${var.agent_name}-start_script"
agentapi_main_script_name = "${var.agent_name}-main_script"

module_dir_path = "$HOME/${var.module_dir_name}"
}

module "agent-helper" {
source = "git::https://github.com/coder/registry.git//registry/coder/modules/agent-helper?ref=35C4n0r/feat-agent-helper-module"
agent_id = var.agent_id
agent_name = var.agent_name
module_dir_name = var.module_dir_name
pre_install_script = var.pre_install_script
install_script = var.install_script
post_install_script = var.post_install_script
start_script = var.start_script
}

resource "coder_script" "agentapi" {
count = var.enable_agentapi ? 1 : 0
agent_id = var.agent_id
display_name = "Install and start AgentAPI"
display_name = "Start AgentAPI"
icon = var.web_app_icon
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail

trap 'coder exp sync complete ${local.agentapi_main_script_name}' EXIT
coder exp sync want ${local.agentapi_main_script_name} ${local.start_script_name}
coder exp sync start ${local.agentapi_main_script_name}

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

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_SERVER_TYPE='${var.agent_name}' \
ARG_AGENTAPI_TERM_WIDTH='${var.agentapi_term_width}' \
ARG_AGENTAPI_TERM_HEIGHT='${var.agentapi_term_height}' \
ARG_AGENTAPI_INITIAL_PROMPT="$(echo -n '${local.encoded_initial_prompt}' | base64 -d)" \
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}' \
Expand All @@ -215,6 +262,7 @@ resource "coder_script" "agentapi" {
}

resource "coder_script" "agentapi_shutdown" {
count = var.enable_agentapi ? 1 : 0
agent_id = var.agent_id
display_name = "AgentAPI Shutdown"
icon = var.web_app_icon
Expand All @@ -234,6 +282,7 @@ resource "coder_script" "agentapi_shutdown" {
}

resource "coder_app" "agentapi_web" {
count = var.enable_agentapi ? 1 : 0
slug = var.web_app_slug
display_name = var.web_app_display_name
agent_id = var.agent_id
Expand All @@ -249,7 +298,7 @@ resource "coder_app" "agentapi_web" {
}
}

resource "coder_app" "agentapi_cli" {
resource "coder_app" "agent_cli" {
count = var.cli_app ? 1 : 0

slug = var.cli_app_slug
Expand All @@ -262,13 +311,17 @@ resource "coder_app" "agentapi_cli" {
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

%{if var.enable_agentapi~}
agentapi attach
%{else}
${local.module_dir_path}/agent-command.sh
%{endif}
EOT
icon = var.cli_app_icon
order = var.cli_app_order
group = var.cli_app_group
}

output "task_app_id" {
value = coder_app.agentapi_web.id
value = var.enable_agentapi ? coder_app.agentapi_web[0].id : null
}
Loading
Loading