Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 2 additions & 61 deletions registry/coder/modules/agentapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand All @@ -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/<module_dir_name>/` 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:

Expand All @@ -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).
7 changes: 0 additions & 7 deletions registry/coder/modules/agentapi/agentapi.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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"
Expand Down
171 changes: 24 additions & 147 deletions registry/coder/modules/agentapi/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ interface SetupProps {
moduleVariables?: Record<string, string>;
}

const moduleDirName = ".agentapi-module";
const moduleDirectory = "/home/coder/.agentapi-module";

const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
Expand All @@ -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,
},
Expand All @@ -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 };
};

Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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`,
]);
};

Expand Down Expand Up @@ -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",
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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");
});
});
});
Loading
Loading