Compare commits

...

53 Commits

Author SHA1 Message Date
35C4n0r
65c6d67547
wip 2026-03-24 15:55:55 +05:30
35C4n0r
b92045181e
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv
# Conflicts:
#	registry/coder/modules/agentapi/README.md
#	registry/coder/modules/agentapi/main.test.ts
#	registry/coder/modules/agentapi/main.tf
#	registry/coder/modules/agentapi/scripts/main.sh
#	registry/coder/modules/agentapi/testdata/agentapi-start.sh
2026-03-23 22:59:44 +05:30
35C4n0r
71c5a4fe11
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv 2026-02-27 15:11:37 +05:30
DevCats
f14d174afc
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv 2026-02-25 12:24:35 -06:00
35C4n0r
96a069a6de
chore: fix readme version 2026-02-13 22:27:54 +05:30
35C4n0r
4c373ff208
chore: update variable name in README.md 2026-02-13 22:26:41 +05:30
35C4n0r
bb03aae627
feat: point source to registry 2026-02-13 22:18:18 +05:30
35C4n0r
d9fd6453dd
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv 2026-02-13 22:07:42 +05:30
35C4n0r
3dddb96b20
bun fmt 2026-02-09 22:54:28 +05:30
35C4n0r
a35df647ec
feat: remove unused variables 2026-02-09 22:49:26 +05:30
35C4n0r
fc88b54bf7
feat: use outputs instead of hard coded names 2026-02-09 21:39:28 +05:30
35C4n0r
64c6417cf5
feat: remove depends_on 2026-02-09 21:02:01 +05:30
35C4n0r
08fcdcc52e
revert 2026-02-09 21:00:58 +05:30
35C4n0r
873c929d86
feat: use outputs instead of hard coded names 2026-02-09 20:51:39 +05:30
35C4n0r
01873e33c0 feat: agentapi tftests 2026-02-09 11:09:09 +00:00
35C4n0r
b14bbe432d
fix: fix tests 2026-02-09 09:19:17 +05:30
35C4n0r
e499d602fd
chore: bun fmt 2026-02-09 08:55:19 +05:30
35C4n0r
0f11e4a5f8
chore: bun fmt 2026-02-09 08:47:33 +05:30
35C4n0r
c2958d58ca
debug 2026-02-07 11:15:29 +05:30
35C4n0r
5de7928e02
debug 2026-02-07 11:05:58 +05:30
35C4n0r
4732deb327 debug 2026-02-07 04:55:38 +00:00
35C4n0r
191f4ca2c2 debug 2026-02-07 04:48:57 +00:00
35C4n0r
fabb5b2f2e debug 2026-02-07 04:37:23 +00:00
35C4n0r
21757f35d7 debug 2026-02-07 04:32:05 +00:00
35C4n0r
c1a3ed53a9 wip: move scripts to agentapi module 2026-02-07 04:08:13 +00:00
35C4n0r
fde6ae6dc1
debug: depends on 2026-02-07 09:21:07 +05:30
35C4n0r
88b01dba31
debug: depends on 2026-02-07 09:16:23 +05:30
35C4n0r
253f95ff5b
debug 2026-02-07 08:55:35 +05:30
35C4n0r
32fa6cb194
debug 2026-02-07 08:52:29 +05:30
35C4n0r
4bbc6d929e
feat: move install and start script logic to agentapi via agent-helper 2026-02-06 23:37:59 +05:30
35C4n0r
c07954592b chore: fix tests 2026-02-06 17:39:58 +00:00
35C4n0r
65c40ed0ad feat: rector agentapi_server_type to agent_name 2026-02-06 17:32:21 +00:00
35C4n0r
df2d72f608 feat: add mock coder since we now depend on coder command to sync scripts 2026-02-06 17:17:57 +00:00
35C4n0r
03ac608e1f
feat: overwrite agentapi logs instead of appending them 2026-02-06 21:34:25 +05:30
35C4n0r
85e7da3c41
fix: complete commands 2026-02-06 19:24:10 +05:30
35C4n0r
5edf4a9c00
fix: complete commands 2026-02-06 19:20:10 +05:30
35C4n0r
c6307226a7
fix: fix typo 2026-02-06 19:14:53 +05:30
35C4n0r
15ca0aa058
feat: sync main.sh 2026-02-06 18:43:13 +05:30
35C4n0r
d6355834d5
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv 2026-02-05 21:54:47 +05:30
35C4n0r
4459d39529 feat: remove the responsibility of running install and start script from agentapi module 2026-02-05 16:17:12 +00:00
35C4n0r
80f47d09dd chore: bump module versions (major) 2026-02-04 16:18:09 +00:00
35C4n0r
f52ef07762
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv 2026-02-04 21:18:42 +05:30
35C4n0r
a3321811c1 chore: remove comments 2026-02-04 15:48:22 +00:00
35C4n0r
63c5e2cf7b chore: improve doc 2026-02-04 15:46:43 +00:00
35C4n0r
ac1fb953fc chore: improve doc 2026-02-04 14:29:15 +00:00
35C4n0r
3d4d24bdfc chore: revert claude changes 2026-02-04 14:16:28 +00:00
35C4n0r
b580ec2abb
Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv 2026-02-04 16:13:25 +05:30
35C4n0r
833f0ac34a chore: fix tests 2026-02-04 10:34:24 +00:00
35C4n0r
6db604bd67
chore: bun fmt 2026-02-04 15:32:51 +05:30
35C4n0r
787ce3fe0c
fix: add start.sh log to the agentapi-start.logs too. 2026-02-04 15:20:27 +05:30
35C4n0r
f93b366605
fix: change agentapi source 2026-02-04 14:56:25 +05:30
35C4n0r
3f68310880
feat(coder/modules/agentapi): enhance start script and configuration options for AgentAPI server 2026-02-04 08:10:36 +05:30
35C4n0r
6e18248731
feat(coder/modules/agentapi): enhance start script and configuration options for AgentAPI server 2026-02-03 18:58:38 +05:30
8 changed files with 434 additions and 108 deletions

View File

@ -16,36 +16,21 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf ```tf
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "2.3.0" version = "3.0.0"
agent_id = var.agent_id agent_id = var.agent_id
web_app_slug = local.app_slug web_app_slug = local.app_slug
web_app_order = var.order web_app_order = var.order
web_app_group = var.group web_app_group = var.group
web_app_icon = var.icon web_app_icon = var.icon
web_app_display_name = "Goose" web_app_display_name = "ClaudeCode"
cli_app_slug = "goose-cli" cli_app_slug = "claude-cli"
cli_app_display_name = "Goose CLI" cli_app_display_name = "Claude CLI"
module_dir_name = local.module_dir_name module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi install_agentapi = var.install_agentapi
pre_install_script = var.pre_install_script agent_name = "claude"
post_install_script = var.post_install_script agentapi_term_width = 67
start_script = local.start_script agentapi_term_height = 1190
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
} }
``` ```
@ -132,3 +117,21 @@ This ensures only the agent process is sandboxed while agentapi itself runs unre
## For module developers ## 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). 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.

View File

@ -6,7 +6,12 @@ import {
setDefaultTimeout, setDefaultTimeout,
beforeAll, beforeAll,
} from "bun:test"; } from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test"; import {
execContainer,
readFileContainer,
runTerraformInit,
runTerraformApply,
} from "~test";
import { import {
loadTestFile, loadTestFile,
writeExecutable, writeExecutable,
@ -58,9 +63,13 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
cli_app_display_name: "AgentAPI CLI", cli_app_display_name: "AgentAPI CLI",
cli_app_slug: "agentapi-cli", cli_app_slug: "agentapi-cli",
agentapi_version: "latest", agentapi_version: "latest",
agent_name: "claude",
module_dir_name: moduleDirName, module_dir_name: moduleDirName,
start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"),
folder: projectDir, folder: projectDir,
pre_install_script: "echo 'Pre-install'",
install_script: "echo 'Install'",
post_install_script: "echo 'Post-install'",
start_script: "echo 'Start'",
...props?.moduleVariables, ...props?.moduleVariables,
}, },
registerCleanup, registerCleanup,
@ -68,11 +77,23 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
skipAgentAPIMock: props?.skipAgentAPIMock, skipAgentAPIMock: props?.skipAgentAPIMock,
moduleDir: import.meta.dir, moduleDir: import.meta.dir,
}); });
// Create the ai agent mock binary
await writeExecutable({ await writeExecutable({
containerId: id, containerId: id,
filePath: "/usr/bin/aiagent", filePath: "/usr/bin/aiagent",
content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"), 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 }; return { id };
}; };
@ -104,36 +125,6 @@ describe("agentapi", async () => {
await expectAgentAPIStarted(id, 3827); 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 () => { test("install-agentapi", async () => {
const { id } = await setup({ skipAgentAPIMock: true }); const { id } = await setup({ skipAgentAPIMock: true });
@ -160,12 +151,12 @@ describe("agentapi", async () => {
expect(respModuleScript.exitCode).toBe(0); expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id); await expectAgentAPIStarted(id);
const agentApiStartLog = await readFileContainer( const agentApiMockLog = await readFileContainer(
id, id,
"/home/coder/test-agentapi-start.log", "/home/coder/agentapi-mock.log",
); );
expect(agentApiStartLog).toContain( expect(agentApiMockLog).toContain(
"Using AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat", "AGENTAPI_CHAT_BASE_PATH: /@default/default.foo/apps/agentapi-web/chat",
); );
}); });
@ -258,6 +249,38 @@ describe("agentapi", async () => {
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *"); 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("state-persistence-disabled", async () => { test("state-persistence-disabled", async () => {
const { id } = await setup({ const { id } = await setup({
moduleVariables: { moduleVariables: {

View File

@ -110,6 +110,12 @@ variable "start_script" {
description = "Script that starts AgentAPI." 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" { variable "install_agentapi" {
type = bool type = bool
description = "Whether to install AgentAPI." description = "Whether to install AgentAPI."
@ -128,6 +134,29 @@ variable "agentapi_port" {
default = 3284 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" { variable "task_log_snapshot" {
type = bool type = bool
description = "Capture last 10 messages when workspace stops for offline viewing while task is paused." description = "Capture last 10 messages when workspace stops for offline viewing while task is paused."
@ -162,6 +191,21 @@ variable "agentapi_subdomain" {
variable "module_dir_name" { variable "module_dir_name" {
type = string type = string
description = "Name of the subdirectory in the home directory for module files." description = "Name of the subdirectory in the home directory for module files."
default = null
validation {
condition = var.module_dir_name == null || var.module_dir_path == null
error_message = "Cannot set both module_dir_name and module_dir_path. Please set only one of them to specify the module directory location."
}
}
variable "module_dir_path" {
type = string
description = "Path to the module directory."
default = null
validation {
condition = var.module_dir_name == null || var.module_dir_path == null
error_message = "Cannot set both module_dir_name and module_dir_path. Please set only one of them to specify the module directory location."
}
} }
variable "enable_boundary" { variable "enable_boundary" {
@ -222,10 +266,7 @@ resource "coder_env" "boundary_config" {
locals { locals {
# we always trim the slash for consistency # we always trim the slash for consistency
workdir = trimsuffix(var.folder, "/") workdir = trimsuffix(var.folder, "/")
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : "" encoded_initial_prompt = var.agentapi_initial_prompt != null ? base64encode(var.agentapi_initial_prompt) : ""
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")) 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. // Chat base path is only set if not using a subdomain.
// NOTE: // NOTE:
@ -238,50 +279,89 @@ locals {
shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh") shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh")
lib_script = file("${path.module}/scripts/lib.sh") lib_script = file("${path.module}/scripts/lib.sh")
boundary_script = file("${path.module}/scripts/boundary.sh") boundary_script = file("${path.module}/scripts/boundary.sh")
agentapi_main_script_name = "${var.agent_name}-main_script"
module_dir_path = var.module_dir_path == null ? "$HOME/.coder-modules/coder/${var.module_dir_name}" : var.module_dir_path
}
module "agent-helper" {
source = "registry.coder.com/coder/agent-helper/coder"
version = "1.0.0"
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" "boundary" {
count = var.enable_boundary ? 1 : 0
agent_id = ""
display_name = ""
script = <<EOT
#!/bin/bash
set -o pipefail
echo -n '${base64encode(local.boundary_script)}' | base64 -d > /tmp/boundary.sh
chmod +x /tmp/boundary.sh
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}' \
/tmp/boundary.sh
EOT
} }
resource "coder_script" "agentapi" { resource "coder_script" "agentapi" {
count = var.enable_agentapi ? 1 : 0
agent_id = var.agent_id agent_id = var.agent_id
display_name = "Install and start AgentAPI" display_name = "Start AgentAPI"
icon = var.web_app_icon icon = var.web_app_icon
script = <<-EOT script = <<-EOT
#!/bin/bash #!/bin/bash
set -o errexit set -o errexit
set -o pipefail set -o pipefail
trap 'coder exp sync complete ${local.agentapi_main_script_name}' EXIT
coder exp sync want ${local.agentapi_main_script_name} ${module.agent-helper.start_script_name}
coder exp sync start ${local.agentapi_main_script_name}
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
chmod +x /tmp/main.sh chmod +x /tmp/main.sh
echo -n '${base64encode(local.lib_script)}' | base64 -d > /tmp/agentapi-lib.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 echo -n '${base64encode(local.boundary_script)}' | base64 -d > /tmp/agentapi-boundary.sh
chmod +x /tmp/agentapi-boundary.sh chmod +x /tmp/agentapi-boundary.sh
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \ ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \ 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_INSTALL_AGENTAPI='${var.install_agentapi}' \
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \ 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_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_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_AGENTAPI_CHAT_BASE_PATH='${local.agentapi_chat_base_path}' \
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \ ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \ 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_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \
ARG_STATE_FILE_PATH='${var.state_file_path}' \ ARG_STATE_FILE_PATH='${var.state_file_path}' \
ARG_PID_FILE_PATH='${var.pid_file_path}' \ ARG_PID_FILE_PATH='${var.pid_file_path}' \
/tmp/main.sh /tmp/main.sh
EOT EOT
run_on_start = true run_on_start = true
depends_on = [module.agent-helper]
} }
resource "coder_script" "agentapi_shutdown" { resource "coder_script" "agentapi_shutdown" {
count = var.enable_agentapi ? 1 : 0
agent_id = var.agent_id agent_id = var.agent_id
display_name = "AgentAPI Shutdown" display_name = "AgentAPI Shutdown"
icon = var.web_app_icon icon = var.web_app_icon
@ -305,6 +385,7 @@ resource "coder_script" "agentapi_shutdown" {
} }
resource "coder_app" "agentapi_web" { resource "coder_app" "agentapi_web" {
count = var.enable_agentapi ? 1 : 0
slug = var.web_app_slug slug = var.web_app_slug
display_name = var.web_app_display_name display_name = var.web_app_display_name
agent_id = var.agent_id agent_id = var.agent_id
@ -320,7 +401,7 @@ resource "coder_app" "agentapi_web" {
} }
} }
resource "coder_app" "agentapi_cli" { resource "coder_app" "agent_cli" {
count = var.cli_app ? 1 : 0 count = var.cli_app ? 1 : 0
slug = var.cli_app_slug slug = var.cli_app_slug
@ -333,7 +414,11 @@ resource "coder_app" "agentapi_cli" {
export LANG=en_US.UTF-8 export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8 export LC_ALL=en_US.UTF-8
%{if var.enable_agentapi~}
agentapi attach agentapi attach
%{else}
${local.module_dir_path}/agent-command.sh
%{endif}
EOT EOT
icon = var.cli_app_icon icon = var.cli_app_icon
order = var.cli_app_order order = var.cli_app_order
@ -341,5 +426,5 @@ resource "coder_app" "agentapi_cli" {
} }
output "task_app_id" { output "task_app_id" {
value = coder_app.agentapi_web.id value = var.enable_agentapi ? coder_app.agentapi_web[0].id : null
} }

View File

@ -0,0 +1,213 @@
# Test agentapi module with default settings (enable_agentapi = true)
run "test_agentapi_defaults" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "claude"
module_dir_name = ".agentapi-module"
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"
install_script = "echo 'install'"
start_script = "echo 'start'"
}
assert {
condition = length(coder_script.agentapi) == 1
error_message = "AgentAPI script should be created when enable_agentapi is true"
}
assert {
condition = coder_script.agentapi[0].agent_id == "test-agent-id"
error_message = "AgentAPI script agent ID should match input"
}
assert {
condition = coder_script.agentapi[0].display_name == "Start AgentAPI"
error_message = "AgentAPI script should have correct display name"
}
assert {
condition = coder_script.agentapi[0].run_on_start == true
error_message = "AgentAPI script should run on start"
}
assert {
condition = length(coder_script.agentapi_shutdown) == 1
error_message = "AgentAPI shutdown script should be created when enable_agentapi is true"
}
assert {
condition = coder_script.agentapi_shutdown[0].run_on_stop == true
error_message = "AgentAPI shutdown script should run on stop"
}
assert {
condition = length(coder_app.agentapi_web) == 1
error_message = "AgentAPI web app should be created when enable_agentapi is true"
}
assert {
condition = coder_app.agentapi_web[0].slug == "agentapi-web"
error_message = "AgentAPI web app slug should match input"
}
assert {
condition = coder_app.agentapi_web[0].subdomain == true
error_message = "AgentAPI web app should use subdomain by default"
}
assert {
condition = length(coder_app.agent_cli) == 0
error_message = "CLI app should not be created by default"
}
}
# Test with enable_agentapi = false
run "test_agentapi_disabled" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "claude"
module_dir_name = ".agentapi-module"
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"
install_script = "echo 'install'"
start_script = "echo 'start'"
enable_agentapi = false
}
assert {
condition = length(coder_script.agentapi) == 0
error_message = "AgentAPI script should not be created when enable_agentapi is false"
}
assert {
condition = length(coder_script.agentapi_shutdown) == 0
error_message = "AgentAPI shutdown script should not be created when enable_agentapi is false"
}
assert {
condition = length(coder_app.agentapi_web) == 0
error_message = "AgentAPI web app should not be created when enable_agentapi is false"
}
}
# Test with CLI app enabled
run "test_cli_app_enabled" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "claude"
module_dir_name = ".agentapi-module"
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"
install_script = "echo 'install'"
start_script = "echo 'start'"
cli_app = true
}
assert {
condition = length(coder_app.agent_cli) == 1
error_message = "CLI app should be created when cli_app is true"
}
assert {
condition = coder_app.agent_cli[0].slug == "agentapi-cli"
error_message = "CLI app slug should match input"
}
assert {
condition = coder_app.agent_cli[0].display_name == "AgentAPI CLI"
error_message = "CLI app display name should match input"
}
}
# Test custom port
run "test_custom_port" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "claude"
module_dir_name = ".agentapi-module"
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"
install_script = "echo 'install'"
start_script = "echo 'start'"
agentapi_port = 4000
}
assert {
condition = coder_app.agentapi_web[0].url == "http://localhost:4000/"
error_message = "AgentAPI web app URL should use custom port"
}
assert {
condition = one([for h in coder_app.agentapi_web[0].healthcheck : h.url]) == "http://localhost:4000/status"
error_message = "AgentAPI healthcheck URL should use custom port"
}
}
# Test subdomain false validation rejects old versions
run "test_subdomain_false_rejects_old_version" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "claude"
module_dir_name = ".agentapi-module"
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"
install_script = "echo 'install'"
start_script = "echo 'start'"
agentapi_subdomain = false
agentapi_version = "v0.3.2"
}
expect_failures = [
var.agentapi_subdomain,
]
}
# Test subdomain false with valid version
run "test_subdomain_false_allows_valid_version" {
command = plan
variables {
agent_id = "test-agent-id"
agent_name = "claude"
module_dir_name = ".agentapi-module"
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"
install_script = "echo 'install'"
start_script = "echo 'start'"
agentapi_subdomain = false
agentapi_version = "v0.3.3"
}
assert {
condition = coder_app.agentapi_web[0].subdomain == false
error_message = "AgentAPI web app should not use subdomain"
}
}

View File

@ -11,14 +11,14 @@ max_attempts=150
agentapi_started=false agentapi_started=false
echo "Waiting for agentapi server to start on port $port..." echo "Waiting for agentapi server to start on port ${port}..."
for i in $(seq 1 "$max_attempts"); do for i in $(seq 1 "${max_attempts}"); do
for j in $(seq 1 3); do for j in $(seq 1 3); do
sleep 0.1 sleep 0.1
if curl -fs -o /dev/null "http://localhost:$port/status"; then if curl -fs -o /dev/null "http://localhost:${port}/status"; then
echo "agentapi response received ($j/3)" echo "agentapi response received (${j}/3)"
else else
echo "agentapi server not responding ($i/$max_attempts)" echo "agentapi server not responding (${i}/${max_attempts})"
continue 2 continue 2
fi fi
done done
@ -26,9 +26,9 @@ for i in $(seq 1 "$max_attempts"); do
break break
done done
if [ "$agentapi_started" != "true" ]; then if [[ "${agentapi_started}" != "true" ]]; then
echo "Error: agentapi server did not start on port $port after $max_attempts attempts." echo "Error: agentapi server did not start on port ${port} after ${max_attempts} attempts."
exit 1 exit 1
fi fi
echo "agentapi server started on port $port." echo "agentapi server started on port ${port}."

View File

@ -5,14 +5,14 @@ set -x
set -o nounset set -o nounset
MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME" MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME"
WORKDIR="$ARG_WORKDIR" WORKDIR="$ARG_WORKDIR"
PRE_INSTALL_SCRIPT="$ARG_PRE_INSTALL_SCRIPT"
INSTALL_SCRIPT="$ARG_INSTALL_SCRIPT"
INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI" INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI"
AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION" AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION"
START_SCRIPT="$ARG_START_SCRIPT"
WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT" WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT"
POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT"
AGENTAPI_PORT="$ARG_AGENTAPI_PORT" AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
AGENTAPI_SERVER_TYPE="$ARG_AGENTAPI_SERVER_TYPE"
AGENTAPI_TERM_WIDTH="$ARG_AGENTAPI_TERM_WIDTH"
AGENTAPI_TERM_HEIGHT="$ARG_AGENTAPI_TERM_HEIGHT"
AGENTAPI_INITIAL_PROMPT="${ARG_AGENTAPI_INITIAL_PROMPT:-}"
AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}" AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}"
TASK_ID="${ARG_TASK_ID:-}" TASK_ID="${ARG_TASK_ID:-}"
TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}" TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}"
@ -48,17 +48,6 @@ if [ ! -d "${WORKDIR}" ]; then
mkdir -p "${WORKDIR}" mkdir -p "${WORKDIR}"
echo "Folder created successfully." echo "Folder created successfully."
fi 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 # Install AgentAPI if enabled
if [ "${INSTALL_AGENTAPI}" = "true" ]; then if [ "${INSTALL_AGENTAPI}" = "true" ]; then
@ -96,18 +85,9 @@ if ! command_exists agentapi; then
exit 1 exit 1
fi 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" 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" 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
export LANG=en_US.UTF-8 export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8 export LC_ALL=en_US.UTF-8
@ -138,5 +118,20 @@ if [ "${ENABLE_STATE_PERSISTENCE}" = "true" ]; then
echo "Warning: State persistence requires agentapi >= v0.12.0 (current: ${actual_version:-unknown}), skipping." echo "Warning: State persistence requires agentapi >= v0.12.0 (current: ${actual_version:-unknown}), skipping."
fi fi
fi fi
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &> "$module_path/agentapi-start.log" &
# Build agentapi server command arguments
ARGS=(
"server"
"--type" "${AGENTAPI_SERVER_TYPE}"
"--port" "${AGENTAPI_PORT}"
"--term-width" "${AGENTAPI_TERM_WIDTH}"
"--term-height" "${AGENTAPI_TERM_HEIGHT}"
)
if [ -n "${AGENTAPI_INITIAL_PROMPT}" ]; then
ARGS+=("--initial-prompt" "${AGENTAPI_INITIAL_PROMPT}")
fi
# Start agentapi server with the agent-command.sh script
nohup agentapi "${ARGS[@]}" -- "$module_path/agent-command.sh" &> "$module_path/agentapi-start.log" &
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}" "$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"

View File

@ -115,6 +115,13 @@ export const setup = async (
}); });
props.registerCleanup(cleanup); props.registerCleanup(cleanup);
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]); await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
// Add a mock coder CLI so that `coder exp sync` commands in the
// startup script succeed inside the test container.
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: "#!/bin/bash\nexit 0",
});
if (!props?.skipAgentAPIMock) { if (!props?.skipAgentAPIMock) {
await writeExecutable({ await writeExecutable({
containerId: id, containerId: id,

View File

@ -14,7 +14,7 @@ if (args.includes("--version")) {
console.log(`starting server on port ${port}`); console.log(`starting server on port ${port}`);
fs.writeFileSync( fs.writeFileSync(
"/home/coder/agentapi-mock.log", "/home/coder/agentapi-mock.log",
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`, `AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}\nAGENTAPI_CHAT_BASE_PATH: ${process.env.AGENTAPI_CHAT_BASE_PATH || "not set"}`,
); );
// Log state persistence env vars. // Log state persistence env vars.