From 849e980bf34fc51091a80977db8c17d79da75347 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Fri, 6 Mar 2026 11:19:22 -0800 Subject: [PATCH] refactor: remove Docker install wrapper, tarballs replace it (#2244) Docker delivery is superseded by the tarball approach (#2232) which is faster (curl|tar ~5-15s vs Docker install ~30s + pull ~60s) and works on every cloud without Docker as a dependency. - Remove tryInstallFromDocker, withDockerInstall, DOCKER_IMAGE_PREFIX - Remove dockerImage and slowInstall from AgentConfig - Remove Docker cloud-init from DigitalOcean - Unwrap openclaw and zeroclaw to direct install (tarball is tried first in orchestrate.ts, these are the fallback) Co-authored-by: Claude Opus 4.6 --- packages/cli/package.json | 2 +- packages/cli/src/digitalocean/digitalocean.ts | 22 +---- packages/cli/src/digitalocean/main.ts | 2 +- packages/cli/src/shared/agent-setup.ts | 85 +------------------ packages/cli/src/shared/agents.ts | 5 -- 5 files changed, 9 insertions(+), 107 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0c8aaad9..bfaa4ca2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.15.0", + "version": "0.15.1", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/digitalocean/digitalocean.ts b/packages/cli/src/digitalocean/digitalocean.ts index 6e3bcfbe..a70f1175 100644 --- a/packages/cli/src/digitalocean/digitalocean.ts +++ b/packages/cli/src/digitalocean/digitalocean.ts @@ -740,29 +740,16 @@ export async function promptDoRegion(): Promise { // ─── Provisioning ──────────────────────────────────────────────────────────── -function getCloudInitUserdata(tier: CloudInitTier = "full", agentName?: string): string { +function getCloudInitUserdata(tier: CloudInitTier = "full"): string { const packages = getPackagesForTier(tier); const lines = [ "#!/bin/bash", "set -e", "export HOME=/root", "export DEBIAN_FRONTEND=noninteractive", + "apt-get update -y", + `apt-get install -y --no-install-recommends ${packages.join(" ")}`, ]; - - if (agentName) { - // Install Docker FIRST (uses apt internally), then start image pull in background. - // The pull runs in parallel with the remaining apt-get/node/bun installs below. - if (!/^[a-z0-9-]+$/.test(agentName)) { - throw new Error(`Invalid agent name: ${agentName}`); - } - lines.push( - "curl -fsSL https://get.docker.com | sh", - `nohup docker pull "ghcr.io/openrouterteam/spawn-${agentName}:latest" > /tmp/docker-pull.log 2>&1 &`, - ); - } - - // Install remaining packages (runs in parallel with docker pull above) - lines.push("apt-get update -y", `apt-get install -y --no-install-recommends ${packages.join(" ")}`); if (needsNode(tier)) { lines.push(`${NODE_INSTALL_CMD} || true`); } @@ -784,7 +771,6 @@ export async function createServer( tier?: CloudInitTier, dropletSize?: string, region?: string, - agentName?: string, ): Promise { const size = dropletSize || process.env.DO_DROPLET_SIZE || "s-2vcpu-4gb"; const effectiveRegion = region || process.env.DO_REGION || "nyc3"; @@ -811,7 +797,7 @@ export async function createServer( size, image, ssh_keys: sshKeyIds, - user_data: getCloudInitUserdata(tier, agentName), + user_data: getCloudInitUserdata(tier), backups: false, monitoring: false, }); diff --git a/packages/cli/src/digitalocean/main.ts b/packages/cli/src/digitalocean/main.ts index 4d847687..fd84b445 100644 --- a/packages/cli/src/digitalocean/main.ts +++ b/packages/cli/src/digitalocean/main.ts @@ -57,7 +57,7 @@ async function main() { }, async createServer(name: string, spawnId?: string) { process.env.SPAWN_ID = spawnId || ""; - await createDroplet(name, agent.cloudInitTier, dropletSize, region, agent.slowInstall ? agentName : undefined); + await createDroplet(name, agent.cloudInitTier, dropletSize, region); }, getServerName, async waitForReady() { diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 7ec0a1cc..827ff299 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -524,75 +524,6 @@ const NPM_PREFIX_SETUP = 'mkdir -p ~/.npm-global/bin; _NPM_G_FLAGS="--prefix $HOME/.npm-global"; fi; ' + 'export PATH="$HOME/.npm-global/bin:$PATH"'; -// ─── Docker Image Extraction ────────────────────────────────────────────────── - -const DOCKER_IMAGE_PREFIX = "ghcr.io/openrouterteam/spawn-"; - -/** - * Try to extract a pre-built agent from a Docker image pulled during cloud-init. - * Returns true if extraction succeeded, false if Docker/image unavailable. - * - * How it works: - * 1. Check if Docker is installed and the image was pulled - * 2. Create a temporary container from the image - * 3. Copy /root/. from the container to /root/ on the host - * 4. Remove the temporary container - * - * The agent then runs natively on the host (not in a container). - */ -async function tryInstallFromDocker(runner: CloudRunner, agentName: string, dockerImage: string): Promise { - logStep(`Checking for pre-built Docker image (${agentName})...`); - const script = [ - // Bail if Docker isn't installed - "command -v docker >/dev/null 2>&1 || { echo '==> Docker not installed'; exit 1; }", - // Poll for image availability — cloud-init started the pull in the background. - // We can't rely on pgrep because the docker CLI exits while dockerd continues pulling. - "_elapsed=0; _max=300", - `while ! docker images -q "${dockerImage}" 2>/dev/null | grep -q .; do`, - ` if [ $_elapsed -ge $_max ]; then echo "==> Timed out waiting for ${dockerImage}"; exit 1; fi`, - ' if [ $_elapsed -eq 0 ]; then echo "==> Waiting for Docker image pull to complete..."; fi', - " sleep 5; _elapsed=$((_elapsed + 5))", - "done", - // Create temp container, copy only known agent directories, clean up - `_cid=$(docker create "${dockerImage}")`, - 'docker cp "$_cid":/root/.claude /root/ 2>/dev/null || true', - 'docker cp "$_cid":/root/.bun /root/ 2>/dev/null || true', - 'docker cp "$_cid":/root/.local /root/ 2>/dev/null || true', - 'docker cp "$_cid":/root/.npm /root/ 2>/dev/null || true', - 'docker cp "$_cid":/root/.cargo /root/ 2>/dev/null || true', - 'docker cp "$_cid":/root/.opencode /root/ 2>/dev/null || true', - 'docker rm "$_cid" >/dev/null', - 'echo "==> Agent extracted from Docker image"', - ].join("\n"); - - try { - await runner.runServer(script, 600); - logInfo(`${agentName} extracted from Docker image`); - return true; - } catch { - logInfo("Docker image not available, falling back to normal install"); - return false; - } -} - -/** - * Wrap an agent's install function with Docker image extraction. - * Tries Docker first; falls back to the original install if unavailable. - */ -function withDockerInstall( - runner: CloudRunner, - agentName: string, - dockerImage: string, - originalInstall: () => Promise, -): () => Promise { - return async () => { - const extracted = await tryInstallFromDocker(runner, agentName, dockerImage); - if (!extracted) { - await originalInstall(); - } - }; -} - // ─── Default Agent Definitions ─────────────────────────────────────────────── const ZEROCLAW_INSTALL_URL = @@ -603,7 +534,6 @@ function createAgents(runner: CloudRunner): Record { claude: { name: "Claude Code", cloudInitTier: "minimal", - dockerImage: `${DOCKER_IMAGE_PREFIX}claude:latest`, preProvision: promptGithubAuth, install: () => installClaudeCode(runner), envVars: (apiKey) => [ @@ -622,7 +552,6 @@ function createAgents(runner: CloudRunner): Record { codex: { name: "Codex CLI", cloudInitTier: "node", - dockerImage: `${DOCKER_IMAGE_PREFIX}codex:latest`, preProvision: promptGithubAuth, install: () => installAgent( @@ -642,12 +571,10 @@ function createAgents(runner: CloudRunner): Record { openclaw: { name: "OpenClaw", cloudInitTier: "full", - dockerImage: `${DOCKER_IMAGE_PREFIX}openclaw:latest`, - slowInstall: true, preProvision: promptGithubAuth, modelPrompt: true, modelDefault: "openrouter/auto", - install: withDockerInstall(runner, "OpenClaw", `${DOCKER_IMAGE_PREFIX}openclaw:latest`, () => + install: () => installAgent( runner, "openclaw", @@ -655,7 +582,6 @@ function createAgents(runner: CloudRunner): Record { "{ grep -qF '.npm-global/bin' ~/.bashrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc; } && " + "{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }", ), - ), envVars: (apiKey) => [ `OPENROUTER_API_KEY=${apiKey}`, `ANTHROPIC_API_KEY=${apiKey}`, @@ -672,7 +598,6 @@ function createAgents(runner: CloudRunner): Record { opencode: { name: "OpenCode", cloudInitTier: "minimal", - dockerImage: `${DOCKER_IMAGE_PREFIX}opencode:latest`, preProvision: promptGithubAuth, install: () => installAgent(runner, "OpenCode", openCodeInstallCmd()), envVars: (apiKey) => [ @@ -684,7 +609,6 @@ function createAgents(runner: CloudRunner): Record { kilocode: { name: "Kilo Code", cloudInitTier: "node", - dockerImage: `${DOCKER_IMAGE_PREFIX}kilocode:latest`, preProvision: promptGithubAuth, install: () => installAgent( @@ -705,10 +629,8 @@ function createAgents(runner: CloudRunner): Record { zeroclaw: { name: "ZeroClaw", cloudInitTier: "minimal", - dockerImage: `${DOCKER_IMAGE_PREFIX}zeroclaw:latest`, - slowInstall: true, preProvision: promptGithubAuth, - install: withDockerInstall(runner, "ZeroClaw", `${DOCKER_IMAGE_PREFIX}zeroclaw:latest`, async () => { + install: async () => { // Add swap before building — low-memory instances (e.g., AWS nano 512 MB) // OOM during Rust compilation if --prefer-prebuilt falls back to source. await ensureSwapSpace(runner); @@ -718,7 +640,7 @@ function createAgents(runner: CloudRunner): Record { `curl --proto '=https' -LsSf ${ZEROCLAW_INSTALL_URL} | bash -s -- --install-rust --install-system-deps --prefer-prebuilt`, 600, // 10 min: swap-backed compilation is slower than the 5-min default ); - }), + }, envVars: (apiKey) => [ `OPENROUTER_API_KEY=${apiKey}`, "ZEROCLAW_PROVIDER=openrouter", @@ -731,7 +653,6 @@ function createAgents(runner: CloudRunner): Record { hermes: { name: "Hermes Agent", cloudInitTier: "minimal", - dockerImage: `${DOCKER_IMAGE_PREFIX}hermes:latest`, preProvision: promptGithubAuth, install: () => installAgent( diff --git a/packages/cli/src/shared/agents.ts b/packages/cli/src/shared/agents.ts index 82b8cea1..4a444a68 100644 --- a/packages/cli/src/shared/agents.ts +++ b/packages/cli/src/shared/agents.ts @@ -29,11 +29,6 @@ export interface AgentConfig { launchCmd: () => string; /** Cloud-init dependency tier. Defaults to "full" if unset. */ cloudInitTier?: CloudInitTier; - /** Docker image for pre-built agent extraction (e.g. "ghcr.io/openrouterteam/spawn-claude:latest"). */ - dockerImage?: string; - /** If true, Docker + image pull are added to cloud-init for faster extraction. - * Only worth it for agents with slow installs (e.g. Rust compilation). */ - slowInstall?: boolean; /** Skip tarball install attempt (e.g., already using snapshot). */ skipTarball?: boolean; }