mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 16:39:50 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
8b99fe0a37
commit
849e980bf3
5 changed files with 9 additions and 107 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.15.0",
|
||||
"version": "0.15.1",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -740,29 +740,16 @@ export async function promptDoRegion(): Promise<string> {
|
|||
|
||||
// ─── 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<void> {
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
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<void>,
|
||||
): () => Promise<void> {
|
||||
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<string, AgentConfig> {
|
|||
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<string, AgentConfig> {
|
|||
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<string, AgentConfig> {
|
|||
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<string, AgentConfig> {
|
|||
"{ 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<string, AgentConfig> {
|
|||
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<string, AgentConfig> {
|
|||
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<string, AgentConfig> {
|
|||
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<string, AgentConfig> {
|
|||
`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<string, AgentConfig> {
|
|||
hermes: {
|
||||
name: "Hermes Agent",
|
||||
cloudInitTier: "minimal",
|
||||
dockerImage: `${DOCKER_IMAGE_PREFIX}hermes:latest`,
|
||||
preProvision: promptGithubAuth,
|
||||
install: () =>
|
||||
installAgent(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue