-
Notifications
You must be signed in to change notification settings - Fork 135
feat(agentapi): add agentapi_cache_dir for persistent binary caching #769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -149,3 +149,4 @@ gha-creds-*.json | |
|
|
||
| # IDEs | ||
| .idea | ||
| .serena | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -149,6 +149,71 @@ 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, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Pre-populate the cache directory with a fake agentapi binary. | ||||||||||||||||||||||
| // The binary is named after the arch and pinned version. | ||||||||||||||||||||||
| const cachedBinary = `${cacheDir}/agentapi-linux-amd64-${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, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const cachedBinary = `${cacheDir}/agentapi-linux-amd64-${pinnedVersion}`; | ||||||||||||||||||||||
|
||||||||||||||||||||||
| const cachedBinary = `${cacheDir}/agentapi-linux-amd64-${pinnedVersion}`; | |
| // Determine the container architecture so the expected cached filename | |
| // matches how the script names the AgentAPI binary. | |
| const archResult = await execContainer(id, ["uname", "-m"]); | |
| expect(archResult.exitCode).toBe(0); | |
| const arch = archResult.stdout.trim(); | |
| const agentArch = | |
| arch === "aarch64" || arch === "arm64" ? "linux-arm64" : "linux-amd64"; | |
| const cachedBinary = `${cacheDir}/agentapi-${agentArch}-${pinnedVersion}`; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
|
@@ -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='${var.agentapi_cache_dir}' \ | ||||||
|
||||||
| ARG_CACHE_DIR='${var.agentapi_cache_dir}' \ | |
| ARG_CACHE_DIR="$(echo -n '${base64encode(var.agentapi_cache_dir)}' | base64 -d)" \ |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -62,24 +63,60 @@ 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]+' || echo "latest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Resolved AgentAPI latest version to: ${resolved_version}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cached_binary="${CACHE_DIR}/${binary_name}-${resolved_version}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "https://github.com/coder/agentapi/releases/latest" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "latest") | |
| echo "Resolved AgentAPI latest version to: ${resolved_version}" | |
| fi | |
| cached_binary="${CACHE_DIR}/${binary_name}-${resolved_version}" | |
| "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}" ] && [ "${resolved_version}" != "latest" ]; then | |
| cached_binary="${CACHE_DIR}/${binary_name}-${resolved_version}" | |
| else | |
| cached_binary="" | |
| fi |
Outdated
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved_version is derived from AGENTAPI_VERSION and then interpolated into a filesystem path. Because agentapi_version can be an arbitrary string, this can introduce path traversal or invalid filenames (e.g., containing /, .., newlines) and cause writes outside CACHE_DIR when caching. Please sanitize the version component before building cached_binary (or restrict agentapi_version to a safe pattern when agentapi_cache_dir is set).
| cached_binary="${CACHE_DIR}/${binary_name}-${resolved_version}" | |
| # Sanitize the resolved version so it is safe to use as a filename component. | |
| # Allow only alphanumerics, dots, underscores, and hyphens; replace others with '_'. | |
| safe_resolved_version=$(printf '%s' "${resolved_version}" | tr -c 'A-Za-z0-9._-' '_') | |
| if [ -z "${safe_resolved_version}" ]; then | |
| echo "Warning: Resolved AgentAPI version '${resolved_version}' is empty after sanitization; skipping cache." | |
| cached_binary="" | |
| else | |
| cached_binary="${CACHE_DIR}/${binary_name}-${safe_resolved_version}" | |
| fi |
Outdated
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caching is an optimization, but with set -e a failure in mkdir -p "${CACHE_DIR}" or cp agentapi "${cached_binary}" will abort the whole install/start even though the binary was successfully downloaded. Make cache writes best-effort (emit a warning and continue) so a misconfigured or read-only cache dir doesn’t break workspace startup.
| mkdir -p "${CACHE_DIR}" | |
| cp agentapi "${cached_binary}" | |
| if ! mkdir -p "${CACHE_DIR}"; then | |
| echo "Warning: Failed to create cache directory ${CACHE_DIR}. Continuing without caching." | |
| elif ! cp agentapi "${cached_binary}"; then | |
| echo "Warning: Failed to cache AgentAPI binary to ${cached_binary}. Continuing without caching." | |
| fi |
Outdated
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing the cache file via cp agentapi "${cached_binary}" is not atomic. If multiple workspaces share the same persistent cache volume, concurrent starts can race and one workspace may read a partially-written binary. Consider writing to a temp file in CACHE_DIR and then mv-ing into place (atomic rename), optionally with a lock to serialize writers.
| cp agentapi "${cached_binary}" | |
| tmp_cached_binary="${cached_binary}.$$" | |
| cp agentapi "${tmp_cached_binary}" | |
| mv -f "${tmp_cached_binary}" "${cached_binary}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test hardcodes the cached binary name as
agentapi-linux-amd64-..., but the install script selectsagentapi-linux-arm64onaarch64. On ARM hosts (e.g., Apple Silicon / Colima), this will miss the cache and exercise the download path unexpectedly. Consider derivingbinary_nameby queryinguname -minside the container and building the expected cache filename accordingly.