feat: ARM tarball builds + arch-aware download (#2248)

* feat: ARM tarball builds + arch-aware download

- Add ARM64 matrix entries for native binary agents (zeroclaw, opencode,
  hermes, claude) in agent-tarballs.yml workflow
- Update agent-tarball.ts to detect remote VM arch via uname -m and
  download the correct tarball (x86_64 or arm64)
- Change release strategy to support multiple arch assets per tag
- Document ARM build requirements in discovery.md for future agents
- Bump CLI version to 0.15.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use sudo for tarball extraction on non-root SSH clouds

On AWS Lightsail, SSH connects as 'ubuntu' (not root), but tarballs
extract to /root/. Without sudo, tar fails with "Permission denied".
Conditionally use sudo when not running as root (id -u != 0).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmed Abushagur 2026-03-06 14:10:33 -08:00 committed by GitHub
parent 5541295012
commit 141254c4e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 22 deletions

View file

@ -54,30 +54,48 @@ export async function tryTarballInstall(
return false;
}
// Find the .tar.gz asset
const asset = parsed.output.assets.find((a) => a.name.endsWith(".tar.gz"));
if (!asset) {
// Find both arch-specific .tar.gz assets and let the remote VM pick the right one.
// We try x86_64 first (most common), and include arm64 fallback in the remote script.
const x86Asset = parsed.output.assets.find((a) => a.name.includes("-x86_64-") && a.name.endsWith(".tar.gz"));
const armAsset = parsed.output.assets.find((a) => a.name.includes("-arm64-") && a.name.endsWith(".tar.gz"));
if (!x86Asset && !armAsset) {
logWarn("No tarball asset found in release");
return false;
}
const url = asset.browser_download_url;
// Build arch-aware download: remote VM detects its own arch and picks the right URL
const x86Url = x86Asset?.browser_download_url || "";
const armUrl = armAsset?.browser_download_url || "";
const url = x86Url || armUrl;
// SECURITY: Validate URL matches expected GitHub releases pattern.
// SECURITY: Validate URLs match expected GitHub releases pattern.
// Prevents shell injection via crafted API responses.
if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/releases\/download\/[^\s'"`;|&$()]+$/.test(url)) {
const urlPattern = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/releases\/download\/[^\s'"`;|&$()]+$/;
if ((x86Url && !urlPattern.test(x86Url)) || (armUrl && !urlPattern.test(armUrl))) {
logWarn("Tarball URL failed safety validation");
return false;
}
logStep("Downloading pre-built agent tarball...");
// Build arch-aware download command: remote VM picks the right URL based on uname -m
// Use sudo for tar extraction — on clouds like AWS Lightsail, SSH user is 'ubuntu' (non-root)
// but tarballs extract to /root/. The ubuntu user has passwordless sudo.
const sudo = '$([ "$(id -u)" != "0" ] && echo sudo || echo "")';
let downloadCmd: string;
if (x86Url && armUrl) {
downloadCmd =
"_arch=$(uname -m); " +
`if [ "$_arch" = "aarch64" ] || [ "$_arch" = "arm64" ]; then ` +
`_url='${armUrl}'; else _url='${x86Url}'; fi; ` +
`curl -fsSL --connect-timeout 10 --max-time 120 "$_url" | ${sudo} tar xz -C / && ${sudo} test -f /root/.spawn-tarball`;
} else {
downloadCmd = `curl -fsSL --connect-timeout 10 --max-time 120 '${url}' | ${sudo} tar xz -C / && ${sudo} test -f /root/.spawn-tarball`;
}
// Download and extract on the remote VM
// --connect-timeout 10s, --max-time 120s, -L to follow redirects (GitHub releases redirect)
await runner.runServer(
`curl -fsSL --connect-timeout 10 --max-time 120 '${url}' | tar xz -C / && [ -f /root/.spawn-tarball ]`,
150, // 2.5 min total timeout for the SSH command
);
await runner.runServer(downloadCmd, 150);
logInfo("Agent installed from pre-built tarball");
return true;