mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28a4d4a4e3 | |||
| b8ae549102 | |||
| fafe8cca7b | |||
| 4517a0c4e0 | |||
| 3d778472e7 | |||
| 36701a3538 | |||
| 0acbcb648e | |||
| b9d352e1ad | |||
| 4d1814a191 | |||
| 968c1c1211 | |||
| ad25115a92 |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
display_name: AgentAPI
|
||||
description: Building block for modules that need to run an AgentAPI server
|
||||
description: Building block for modules that need to run an AgentAPI server.
|
||||
icon: ../../../../.icons/coder.svg
|
||||
verified: true
|
||||
tags: [internal, library]
|
||||
@@ -11,65 +11,47 @@ tags: [internal, library]
|
||||
> [!CAUTION]
|
||||
> We do not recommend using this module directly. Instead, please consider using one of our [Tasks-compatible AI agent modules](https://registry.coder.com/modules?search=tag%3Atasks).
|
||||
|
||||
The AgentAPI module is a building block for modules that need to run an AgentAPI server. It is intended primarily for internal use by Coder to create modules compatible with Tasks.
|
||||
The AgentAPI module is a building block for modules that need to run an [AgentAPI](https://github.com/coder/agentapi) server. It is intended primarily for internal use by Coder to create modules compatible with [Tasks](https://coder.com/docs/ai-coder/tasks).
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.4.0"
|
||||
version = "2.5.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"
|
||||
module_dir_name = local.module_dir_name
|
||||
cli_app_slug = "goose-cli"
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
## Task log snapshot
|
||||
## Features
|
||||
|
||||
Captures the last 10 messages from AgentAPI when a task workspace stops. This allows viewing conversation history while the task is paused.
|
||||
- **Web and CLI apps**: creates `coder_app` resources for browser-based chat and terminal attachment
|
||||
- **Task log snapshot**: captures the last 10 conversation messages when a workspace stops, enabling offline viewing while the task is paused
|
||||
- **State persistence**: optionally saves and restores AgentAPI conversation state across workspace restarts (requires agentapi >= v0.12.0)
|
||||
- **Script orchestration**: uses [coder-utils](https://registry.coder.com/modules/coder/coder-utils) for `coder exp sync` based script ordering so downstream modules can serialize their own scripts behind this module
|
||||
|
||||
To enable for task workspaces:
|
||||
## Examples
|
||||
|
||||
### Task log snapshot
|
||||
|
||||
Enabled by default. Captures the last 10 messages from AgentAPI when a task workspace stops.
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
# ... other config
|
||||
task_log_snapshot = true # default: true
|
||||
task_log_snapshot = true # default
|
||||
}
|
||||
```
|
||||
|
||||
## State Persistence
|
||||
### State persistence
|
||||
|
||||
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`).
|
||||
|
||||
To enable:
|
||||
Disabled by default. Requires agentapi >= v0.12.0.
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
@@ -78,57 +60,38 @@ module "agentapi" {
|
||||
}
|
||||
```
|
||||
|
||||
To override file paths:
|
||||
Custom file paths:
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
# ... other config
|
||||
state_file_path = "/custom/path/state.json"
|
||||
pid_file_path = "/custom/path/agentapi.pid"
|
||||
enable_state_persistence = true
|
||||
state_file_path = "/custom/path/state.json"
|
||||
pid_file_path = "/custom/path/agentapi.pid"
|
||||
}
|
||||
```
|
||||
|
||||
## Boundary (Network Filtering)
|
||||
### Script serialization
|
||||
|
||||
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:
|
||||
The module outputs `scripts`, an ordered list of `coder exp sync` names. Downstream modules can use these to serialize their own `coder_script` resources behind the install pipeline:
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
# ... other config
|
||||
enable_boundary = true
|
||||
boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml"
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
# ...
|
||||
}
|
||||
|
||||
# Optional: install boundary binary instead of using coder subcommand
|
||||
# use_boundary_directly = true
|
||||
# boundary_version = "0.6.0"
|
||||
# compile_boundary_from_source = false
|
||||
output "scripts" {
|
||||
value = module.agentapi.scripts
|
||||
}
|
||||
```
|
||||
|
||||
### 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).
|
||||
For a complete example of how to build a module on top of AgentAPI, see the [Goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Install logs are written to `~/.coder-modules/coder/agentapi/logs/install.log`
|
||||
- AgentAPI server logs are written to `~/.coder-modules/coder/agentapi/agentapi-start.log`
|
||||
- Check `agentapi --version` to verify the installed binary version
|
||||
|
||||
@@ -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" {
|
||||
@@ -29,33 +27,12 @@ run "default_values" {
|
||||
error_message = "pid_file_path should default to empty string"
|
||||
}
|
||||
|
||||
# Verify start script contains state persistence ARG_ vars.
|
||||
assert {
|
||||
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE", coder_script.agentapi.script))
|
||||
error_message = "start script should contain ARG_ENABLE_STATE_PERSISTENCE"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = can(regex("ARG_STATE_FILE_PATH", coder_script.agentapi.script))
|
||||
error_message = "start script should contain ARG_STATE_FILE_PATH"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi.script))
|
||||
error_message = "start script should contain ARG_PID_FILE_PATH"
|
||||
}
|
||||
|
||||
# Verify shutdown script contains PID-related ARG_ vars.
|
||||
assert {
|
||||
condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi_shutdown.script))
|
||||
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"
|
||||
@@ -74,11 +51,10 @@ run "state_persistence_disabled" {
|
||||
error_message = "enable_state_persistence should be false"
|
||||
}
|
||||
|
||||
# Even when disabled, the ARG_ vars should still be in the script
|
||||
# (the shell script handles the conditional logic).
|
||||
# Verify shutdown script contains the disabled flag.
|
||||
assert {
|
||||
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE='false'", coder_script.agentapi.script))
|
||||
error_message = "start script should contain ARG_ENABLE_STATE_PERSISTENCE='false'"
|
||||
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE='false'", coder_script.agentapi_shutdown.script))
|
||||
error_message = "shutdown script should contain ARG_ENABLE_STATE_PERSISTENCE='false'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,19 +66,18 @@ run "custom_paths" {
|
||||
pid_file_path = "/custom/agentapi.pid"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = can(regex("/custom/state.json", coder_script.agentapi.script))
|
||||
error_message = "start script should contain custom state_file_path"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = can(regex("/custom/agentapi.pid", coder_script.agentapi.script))
|
||||
error_message = "start script should contain custom pid_file_path"
|
||||
}
|
||||
|
||||
# Verify custom paths also appear in shutdown script.
|
||||
# Verify custom paths appear in shutdown script.
|
||||
assert {
|
||||
condition = can(regex("/custom/agentapi.pid", coder_script.agentapi_shutdown.script))
|
||||
error_message = "shutdown script should contain custom pid_file_path"
|
||||
}
|
||||
}
|
||||
|
||||
run "scripts_output" {
|
||||
command = plan
|
||||
|
||||
assert {
|
||||
condition = length(output.scripts) == 1 && output.scripts[0] == "coder-agentapi-install_script"
|
||||
error_message = "scripts output should list the install script sync name"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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,
|
||||
},
|
||||
@@ -68,11 +67,31 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
moduleDir: import.meta.dir,
|
||||
});
|
||||
// Mock `coder` CLI so `coder exp sync` calls from coder-utils wrappers
|
||||
// succeed without a real control plane.
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/coder",
|
||||
content: "#!/bin/bash\nexit 0\n",
|
||||
});
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
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 +123,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 +302,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 +386,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 +531,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 +561,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 +575,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 +602,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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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/<module_dir_name>/agentapi-state.json."
|
||||
description = "Path to the AgentAPI state file. Defaults to <module_directory>/agentapi-state.json."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pid_file_path" {
|
||||
type = string
|
||||
description = "Path to the AgentAPI PID file. Defaults to $HOME/<module_dir_name>/agentapi.pid."
|
||||
description = "Path to the AgentAPI PID file. Defaults to <module_directory>/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 {
|
||||
@@ -232,12 +173,7 @@ locals {
|
||||
web_app = var.web_app || local.is_task
|
||||
|
||||
# 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"))
|
||||
workdir = trimsuffix(var.folder, "/")
|
||||
// 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
|
||||
@@ -245,51 +181,38 @@ locals {
|
||||
// - 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")
|
||||
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")
|
||||
|
||||
shutdown_script_destination = "${var.module_directory}/agentapi-shutdown.sh"
|
||||
lib_script_destination = "${var.module_directory}/agentapi-lib.sh"
|
||||
|
||||
install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
|
||||
ARG_MODULE_DIRECTORY = var.module_directory
|
||||
ARG_WORKDIR = local.workdir
|
||||
ARG_INSTALL_AGENTAPI = tostring(var.install_agentapi)
|
||||
ARG_AGENTAPI_VERSION = var.agentapi_version
|
||||
ARG_WAIT_FOR_START_SCRIPT = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||
ARG_AGENTAPI_PORT = tostring(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 = tostring(var.task_log_snapshot)
|
||||
ARG_ENABLE_STATE_PERSISTENCE = tostring(var.enable_state_persistence)
|
||||
ARG_STATE_FILE_PATH = var.state_file_path
|
||||
ARG_PID_FILE_PATH = var.pid_file_path
|
||||
ARG_LIB_SCRIPT = base64encode(local.lib_script)
|
||||
})
|
||||
}
|
||||
|
||||
resource "coder_script" "agentapi" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Install and start AgentAPI"
|
||||
icon = var.web_app_icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
module "coder_utils" {
|
||||
source = "registry.coder.com/coder/coder-utils/coder"
|
||||
version = "0.0.1"
|
||||
|
||||
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
|
||||
|
||||
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_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
|
||||
EOT
|
||||
run_on_start = true
|
||||
agent_id = var.agent_id
|
||||
module_directory = var.module_directory
|
||||
display_name_prefix = "AgentAPI"
|
||||
icon = var.web_app_icon
|
||||
install_script = local.install_script
|
||||
}
|
||||
|
||||
resource "coder_script" "agentapi_shutdown" {
|
||||
@@ -301,17 +224,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
|
||||
}
|
||||
|
||||
@@ -356,3 +281,8 @@ resource "coder_app" "agentapi_cli" {
|
||||
output "task_app_id" {
|
||||
value = local.web_app ? coder_app.agentapi_web[0].id : ""
|
||||
}
|
||||
|
||||
output "scripts" {
|
||||
description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order. Scripts that were not configured are absent from the list."
|
||||
value = module.coder_utils.scripts
|
||||
}
|
||||
|
||||
@@ -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:-}"
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
set -o nounset
|
||||
|
||||
MODULE_DIRECTORY='${ARG_MODULE_DIRECTORY}'
|
||||
WORKDIR='${ARG_WORKDIR}'
|
||||
INSTALL_AGENTAPI='${ARG_INSTALL_AGENTAPI}'
|
||||
AGENTAPI_VERSION='${ARG_AGENTAPI_VERSION}'
|
||||
WAIT_FOR_START_SCRIPT=$(echo -n '${ARG_WAIT_FOR_START_SCRIPT}' | base64 -d)
|
||||
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}'
|
||||
ENABLE_STATE_PERSISTENCE='${ARG_ENABLE_STATE_PERSISTENCE}'
|
||||
STATE_FILE_PATH='${ARG_STATE_FILE_PATH}'
|
||||
PID_FILE_PATH='${ARG_PID_FILE_PATH}'
|
||||
LIB_SCRIPT=$(echo -n '${ARG_LIB_SCRIPT}' | base64 -d)
|
||||
|
||||
set +o nounset
|
||||
|
||||
# Write and source lib.sh
|
||||
LIB_SCRIPT_PATH="$${MODULE_DIRECTORY}/agentapi-lib.sh"
|
||||
echo -n "$LIB_SCRIPT" > "$LIB_SCRIPT_PATH"
|
||||
# shellcheck source=lib.sh
|
||||
source "$LIB_SCRIPT_PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
mkdir -p "$${MODULE_DIRECTORY}/scripts"
|
||||
|
||||
# Check for jq dependency if task log snapshot is enabled.
|
||||
if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then
|
||||
if ! command_exists jq; then
|
||||
echo "Warning: jq is not installed. Task log snapshot requires jq to capture conversation history."
|
||||
echo "Install jq to enable log snapshot functionality when the workspace stops."
|
||||
fi
|
||||
fi
|
||||
if [ ! -d "$${WORKDIR}" ]; then
|
||||
echo "Warning: The specified folder '$${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "$${WORKDIR}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
# Install AgentAPI if enabled
|
||||
if [ "$${INSTALL_AGENTAPI}" = "true" ]; then
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$${AGENTAPI_VERSION}" = "latest" ]; then
|
||||
# for the latest release the download URL pattern is different than for tagged releases
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||
else
|
||||
download_url="https://github.com/coder/agentapi/releases/download/$${AGENTAPI_VERSION}/$binary_name"
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"$download_url"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
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}"
|
||||
|
||||
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_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_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_DIRECTORY}/scripts/agentapi-start.sh" true "$${AGENTAPI_PORT}" &> "$${MODULE_DIRECTORY}/agentapi-start.log" &
|
||||
"$${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" "$${AGENTAPI_PORT}"
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
set -o nounset
|
||||
MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME"
|
||||
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:-}"
|
||||
set +o nounset
|
||||
|
||||
# shellcheck source=lib.sh
|
||||
source /tmp/agentapi-lib.sh
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
module_path="$HOME/${MODULE_DIR_NAME}"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
# Check for jq dependency if task log snapshot is enabled.
|
||||
if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then
|
||||
if ! command_exists jq; then
|
||||
echo "Warning: jq is not installed. Task log snapshot requires jq to capture conversation history."
|
||||
echo "Install jq to enable log snapshot functionality when the workspace stops."
|
||||
fi
|
||||
fi
|
||||
if [ ! -d "${WORKDIR}" ]; then
|
||||
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
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..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
|
||||
# for the latest release the download URL pattern is different than for tagged releases
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||
else
|
||||
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"$download_url"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
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
|
||||
|
||||
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}"
|
||||
# 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_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}"
|
||||
@@ -46,7 +46,25 @@ export const setupContainer = async ({
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
// Find the run_on_start script. With coder-utils the install script lives
|
||||
// inside a module and the shutdown script is a separate resource, so we
|
||||
// pick the first coder_script that has run_on_start = true.
|
||||
let coderScript: { script: string; [k: string]: unknown } | undefined;
|
||||
for (const resource of state.resources) {
|
||||
if (resource.type !== "coder_script") continue;
|
||||
for (const instance of resource.instances) {
|
||||
const attrs = instance.attributes as Record<string, unknown>;
|
||||
if (attrs.run_on_start === true) {
|
||||
coderScript = attrs as { script: string; [k: string]: unknown };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (coderScript) break;
|
||||
}
|
||||
if (!coderScript) {
|
||||
// Fallback to original behavior for backwards compatibility.
|
||||
coderScript = findResourceInstance(state, "coder_script");
|
||||
}
|
||||
const coderEnvVars = extractCoderEnvVars(state);
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return {
|
||||
|
||||
@@ -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");
|
||||
|
||||
+5
-15
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user