Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,4 @@ gha-creds-*.json

# IDEs
.idea
.serena
15 changes: 14 additions & 1 deletion registry/coder/modules/agentapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.1.1"
version = "2.2.0"

agent_id = var.agent_id
web_app_slug = local.app_slug
Expand Down Expand Up @@ -62,6 +62,19 @@ module "agentapi" {
}
```

## Caching the AgentAPI binary

When `agentapi_cache_dir` is set, the AgentAPI binary is cached to that directory after the first download. On subsequent workspace starts, the cached binary is used instead of re-downloading from GitHub. This is particularly useful when workspaces use a persistent volume.

```tf
module "agentapi" {
# ... other config
agentapi_cache_dir = "/home/coder/.cache/agentapi"
}
```

The cached binary is stored with a name that includes the architecture and version (e.g., `agentapi-linux-amd64-v0.10.0`) so different versions can coexist in the same cache directory.

## 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).
78 changes: 78 additions & 0 deletions registry/coder/modules/agentapi/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,84 @@ describe("agentapi", async () => {
expect(respAgentAPI.exitCode).toBe(0);
});

test("cache-dir-uses-cached-binary", async () => {
// Verify that when a cached binary exists in the cache dir, it is used
// instead of downloading. Use a pinned version so the cache filename is
// deterministic (resolving "latest" requires a network call).
const cacheDir = "/home/coder/.agentapi-cache";
const pinnedVersion = "v0.10.0";
const { id } = await setup({
moduleVariables: {
install_agentapi: "true",
agentapi_cache_dir: cacheDir,
agentapi_version: pinnedVersion,
},
});

// Derive the binary name from the container's architecture so the test is
// portable across amd64 and arm64 hosts.
const archResult = await execContainer(id, ["uname", "-m"]);
expect(archResult.exitCode).toBe(0);
const agentArch =
archResult.stdout.trim() === "aarch64" ? "linux-arm64" : "linux-amd64";

// Pre-populate the cache directory with a fake agentapi binary.
const cachedBinary = `${cacheDir}/agentapi-${agentArch}-${pinnedVersion}`;
await execContainer(id, [
"bash",
"-c",
`mkdir -p ${cacheDir} && cp /usr/bin/agentapi ${cachedBinary}`,
]);

const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
expect(respModuleScript.stdout).toContain(
`Using cached AgentAPI binary from ${cachedBinary}`,
);

await expectAgentAPIStarted(id);
});

test("cache-dir-saves-binary-after-download", async () => {
// Verify that after downloading agentapi, the binary is saved to the cache
// dir under the resolved version name. Use a pinned version so the cache
// filename is deterministic.
const cacheDir = "/home/coder/.agentapi-cache";
const pinnedVersion = "v0.10.0";
const { id } = await setup({
skipAgentAPIMock: true,
moduleVariables: {
agentapi_cache_dir: cacheDir,
agentapi_version: pinnedVersion,
},
});

// Derive the binary name from the container's architecture so the test is
// portable across amd64 and arm64 hosts.
const archResult = await execContainer(id, ["uname", "-m"]);
expect(archResult.exitCode).toBe(0);
const agentArch =
archResult.stdout.trim() === "aarch64" ? "linux-arm64" : "linux-amd64";

const cachedBinary = `${cacheDir}/agentapi-${agentArch}-${pinnedVersion}`;
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
expect(respModuleScript.stdout).toContain(
`Caching AgentAPI binary to ${cachedBinary}`,
);

await expectAgentAPIStarted(id);

// Verify the binary was saved to the cache directory.
const respCacheCheck = await execContainer(id, [
"bash",
"-c",
`test -f ${cachedBinary} && echo "cached"`,
]);
expect(respCacheCheck.exitCode).toBe(0);
expect(respCacheCheck.stdout).toContain("cached");
});

test("no-subdomain-base-path", async () => {
const { id } = await setup({
moduleVariables: {
Expand Down
6 changes: 6 additions & 0 deletions registry/coder/modules/agentapi/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ variable "module_dir_name" {
description = "Name of the subdirectory in the home directory for module files."
}

variable "agentapi_cache_dir" {
type = string
description = "Path to a directory where the AgentAPI binary will be cached after download. On subsequent workspace starts, the cached binary is used instead of downloading again. Useful with persistent volumes to avoid repeated downloads."
default = ""
}

locals {
# we always trim the slash for consistency
Expand Down Expand Up @@ -209,6 +214,7 @@ resource "coder_script" "agentapi" {
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_CACHE_DIR="$(echo -n '${base64encode(var.agentapi_cache_dir)}' | base64 -d)" \
/tmp/main.sh
EOT
run_on_start = true
Expand Down
77 changes: 66 additions & 11 deletions registry/coder/modules/agentapi/scripts/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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}"
CACHE_DIR="${ARG_CACHE_DIR:-}"
set +o nounset

command_exists() {
Expand Down Expand Up @@ -62,24 +63,78 @@ if [ "${INSTALL_AGENTAPI}" = "true" ]; then
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

cached_binary=""
if [ -n "${CACHE_DIR}" ]; then
resolved_version="${AGENTAPI_VERSION}"
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
# Resolve the actual version tag so the cache key is stable (e.g. v0.10.0, not "latest").
# GitHub redirects /releases/latest to /releases/tag/vX.Y.Z; we extract the tag from that URL.
resolved_version=$(curl \
--retry 3 \
--retry-delay 3 \
--fail \
--retry-all-errors \
-Ls \
-o /dev/null \
-w '%{url_effective}' \
"https://github.com/coder/agentapi/releases/latest" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || true)
if [ -z "${resolved_version}" ]; then
echo "Warning: Failed to resolve latest AgentAPI version tag; proceeding without cache for this run."
else
echo "Resolved AgentAPI latest version to: ${resolved_version}"
fi
fi
if [ -n "${resolved_version}" ]; then
# Sanitize the version so it is safe to use as a filename component.
# Allow only alphanumerics, dots, underscores, and hyphens; replace others with '_'.
safe_version=$(printf '%s' "${resolved_version}" | tr -c 'A-Za-z0-9._-' '_')
cached_binary="${CACHE_DIR}/${binary_name}-${safe_version}"
fi
fi

if [ -n "${cached_binary}" ] && [ -f "${cached_binary}" ]; then
echo "Using cached AgentAPI binary from ${cached_binary}"
cp "${cached_binary}" agentapi
chmod +x agentapi
sudo mv agentapi /usr/local/bin/agentapi
else
curl \
--retry 5 \
--retry-delay 5 \
--fail \
--retry-all-errors \
-L \
-C - \
-o agentapi \
"$download_url"
chmod +x agentapi

if [ -n "${cached_binary}" ]; then
echo "Caching AgentAPI binary to ${cached_binary}"
# Write atomically via a temp file so concurrent workspace starts on a shared
# volume never observe a partially-written binary.
if ! mkdir -p "${CACHE_DIR}"; then
echo "Warning: Failed to create cache directory ${CACHE_DIR}. Continuing without caching."
else
tmp_cached_binary="${cached_binary}.$$"
if ! cp agentapi "${tmp_cached_binary}" || ! mv -f "${tmp_cached_binary}" "${cached_binary}"; then
rm -f "${tmp_cached_binary}"
echo "Warning: Failed to cache AgentAPI binary to ${cached_binary}. Continuing without caching."
fi
fi
fi

sudo mv agentapi /usr/local/bin/agentapi
fi
fi
if ! command_exists agentapi; then
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
Expand Down