mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
fix(daytona): tighten live provider behavior
This commit is contained in:
parent
a7be238ef4
commit
c9b663d371
6 changed files with 88 additions and 55 deletions
|
|
@ -831,6 +831,18 @@ function headlessError(
|
|||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a trusted local Spawn checkout path for SPAWN_CLI_DIR.
|
||||
*
|
||||
* On macOS, `/tmp` commonly resolves to `/private/tmp`, so compare against
|
||||
* the checkout's real path instead of the raw env var spelling.
|
||||
*/
|
||||
function resolveTrustedCliDir(cliDir: string): string {
|
||||
const resolvedCliDir = path.resolve(cliDir);
|
||||
const realCliDir = tryCatchIf(isFileError, () => fs.realpathSync(resolvedCliDir));
|
||||
return realCliDir.ok ? realCliDir.data : resolvedCliDir;
|
||||
}
|
||||
|
||||
/** Run a script in headless mode (all output to stderr, no interactive session) */
|
||||
function runScriptHeadless(script: string, prompt?: string, debug?: boolean, spawnName?: string): Promise<number> {
|
||||
validateScriptContent(script);
|
||||
|
|
@ -1006,7 +1018,7 @@ export async function cmdRunHeadless(agent: string, cloud: string, opts: Headles
|
|||
if (cliDir) {
|
||||
const hasBadChars = (s: string) => s.includes("..") || s.includes("/") || s.includes("\\");
|
||||
if (!hasBadChars(resolvedCloud) && !hasBadChars(resolvedAgent)) {
|
||||
const resolvedCliDir = path.resolve(cliDir);
|
||||
const resolvedCliDir = resolveTrustedCliDir(cliDir);
|
||||
const candidatePath = path.join(resolvedCliDir, "packages", "cli", "src", resolvedCloud, "main.ts");
|
||||
const realResult = tryCatchIf(isFileError, () => fs.realpathSync(candidatePath));
|
||||
if (realResult.ok) {
|
||||
|
|
@ -1071,7 +1083,7 @@ export async function cmdRunHeadless(agent: string, cloud: string, opts: Headles
|
|||
const safeAgent = !hasBadChars(resolvedAgent);
|
||||
|
||||
if (safeCloud && safeAgent) {
|
||||
const resolvedCliDir = path.resolve(cliDir);
|
||||
const resolvedCliDir = resolveTrustedCliDir(cliDir);
|
||||
const candidatePath = path.join(resolvedCliDir, "sh", resolvedCloud, `${resolvedAgent}.sh`);
|
||||
const realResult = tryCatchIf(isFileError, () => fs.realpathSync(candidatePath));
|
||||
if (realResult.ok) {
|
||||
|
|
|
|||
|
|
@ -276,36 +276,36 @@ export async function ensureDaytonaAuthenticated(): Promise<void> {
|
|||
}
|
||||
|
||||
function resolveSandboxSizeFromEnv(): SandboxSize | null {
|
||||
const sizeId = process.env.DAYTONA_SANDBOX_SIZE;
|
||||
if (sizeId) {
|
||||
const matched = SANDBOX_SIZES.find((size) => size.id === sizeId);
|
||||
if (!matched) {
|
||||
throw new Error(`Invalid DAYTONA_SANDBOX_SIZE: ${sizeId}`);
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
const cpu = process.env.DAYTONA_CPU;
|
||||
const memory = process.env.DAYTONA_MEMORY;
|
||||
const disk = process.env.DAYTONA_DISK;
|
||||
if (!cpu && !memory && !disk) {
|
||||
if (cpu || memory || disk) {
|
||||
const parsedCpu = Number.parseInt(cpu || String(DEFAULT_SANDBOX_SIZE.cpu), 10);
|
||||
const parsedMemory = Number.parseInt(memory || String(DEFAULT_SANDBOX_SIZE.memory), 10);
|
||||
const parsedDisk = Number.parseInt(disk || String(DEFAULT_SANDBOX_SIZE.disk), 10);
|
||||
if (!Number.isInteger(parsedCpu) || !Number.isInteger(parsedMemory) || !Number.isInteger(parsedDisk)) {
|
||||
throw new Error("DAYTONA_CPU, DAYTONA_MEMORY, and DAYTONA_DISK must be integers");
|
||||
}
|
||||
|
||||
return {
|
||||
id: "custom",
|
||||
cpu: parsedCpu,
|
||||
memory: parsedMemory,
|
||||
disk: parsedDisk,
|
||||
label: `${parsedCpu} vCPU · ${parsedMemory} GiB RAM · ${parsedDisk} GiB disk`,
|
||||
};
|
||||
}
|
||||
|
||||
const sizeId = process.env.DAYTONA_SANDBOX_SIZE;
|
||||
if (!sizeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedCpu = Number.parseInt(cpu || String(DEFAULT_SANDBOX_SIZE.cpu), 10);
|
||||
const parsedMemory = Number.parseInt(memory || String(DEFAULT_SANDBOX_SIZE.memory), 10);
|
||||
const parsedDisk = Number.parseInt(disk || String(DEFAULT_SANDBOX_SIZE.disk), 10);
|
||||
if (!Number.isInteger(parsedCpu) || !Number.isInteger(parsedMemory) || !Number.isInteger(parsedDisk)) {
|
||||
throw new Error("DAYTONA_CPU, DAYTONA_MEMORY, and DAYTONA_DISK must be integers");
|
||||
const matched = SANDBOX_SIZES.find((size) => size.id === sizeId);
|
||||
if (!matched) {
|
||||
throw new Error(`Invalid DAYTONA_SANDBOX_SIZE: ${sizeId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: "custom",
|
||||
cpu: parsedCpu,
|
||||
memory: parsedMemory,
|
||||
disk: parsedDisk,
|
||||
label: `${parsedCpu} vCPU · ${parsedMemory} GiB RAM · ${parsedDisk} GiB disk`,
|
||||
};
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -548,7 +548,11 @@ export async function startGateway(runner: CloudRunner): Promise<void> {
|
|||
"#!/bin/bash",
|
||||
'source "$HOME/.spawnrc" 2>/dev/null',
|
||||
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH"',
|
||||
"exec openclaw gateway",
|
||||
"while true; do",
|
||||
" openclaw gateway",
|
||||
' echo "openclaw gateway exited, restarting in 5s" >> /tmp/openclaw-gateway.log',
|
||||
" sleep 5",
|
||||
"done",
|
||||
].join("\n");
|
||||
|
||||
// __USER__ and __HOME__ are sed-substituted at deploy time
|
||||
|
|
@ -586,11 +590,12 @@ export async function startGateway(runner: CloudRunner): Promise<void> {
|
|||
const script = [
|
||||
"source ~/.spawnrc 2>/dev/null",
|
||||
"export PATH=$HOME/.npm-global/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH",
|
||||
"if command -v systemctl >/dev/null 2>&1; then",
|
||||
"printf '%s' '" + wrapperB64 + "' | base64 -d > /tmp/openclaw-gateway-wrapper.tmp",
|
||||
"chmod +x /tmp/openclaw-gateway-wrapper.tmp",
|
||||
"if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then",
|
||||
' _sudo=""',
|
||||
' [ "$(id -u)" != "0" ] && _sudo="sudo"',
|
||||
" printf '%s' '" + wrapperB64 + "' | base64 -d | $_sudo tee /usr/local/bin/openclaw-gateway-wrapper > /dev/null",
|
||||
" $_sudo chmod +x /usr/local/bin/openclaw-gateway-wrapper",
|
||||
" $_sudo mv /tmp/openclaw-gateway-wrapper.tmp /usr/local/bin/openclaw-gateway-wrapper",
|
||||
" printf '%s' '" + unitB64 + "' | base64 -d > /tmp/openclaw-gateway.unit.tmp",
|
||||
' sed -i "s|__USER__|$(whoami)|;s|__HOME__|$HOME|" /tmp/openclaw-gateway.unit.tmp',
|
||||
" $_sudo mv /tmp/openclaw-gateway.unit.tmp /etc/systemd/system/openclaw-gateway.service",
|
||||
|
|
@ -601,13 +606,13 @@ export async function startGateway(runner: CloudRunner): Promise<void> {
|
|||
' [ "$(id -u)" != "0" ] && _cron_restart="sudo systemctl restart openclaw-gateway"',
|
||||
' (crontab -l 2>/dev/null | grep -v openclaw-gateway; echo "0 * * * * nc -z 127.0.0.1 18789 2>/dev/null || $_cron_restart >> /tmp/openclaw-gateway.log 2>&1") | crontab - 2>/dev/null || true',
|
||||
"else",
|
||||
' _oc_bin=$(command -v openclaw) || { echo "openclaw not found in PATH"; exit 1; }',
|
||||
" mv /tmp/openclaw-gateway-wrapper.tmp /tmp/openclaw-gateway-wrapper",
|
||||
` if ${portCheck}; then echo "Gateway already running"; exit 0; fi`,
|
||||
' if command -v setsid >/dev/null 2>&1; then setsid "$_oc_bin" gateway > /tmp/openclaw-gateway.log 2>&1 < /dev/null &',
|
||||
' else nohup "$_oc_bin" gateway > /tmp/openclaw-gateway.log 2>&1 < /dev/null & fi',
|
||||
" if command -v setsid >/dev/null 2>&1; then setsid /tmp/openclaw-gateway-wrapper > /tmp/openclaw-gateway.log 2>&1 < /dev/null &",
|
||||
" else nohup /tmp/openclaw-gateway-wrapper > /tmp/openclaw-gateway.log 2>&1 < /dev/null & fi",
|
||||
"fi",
|
||||
"elapsed=0; while [ $elapsed -lt 300 ]; do",
|
||||
` if ${portCheck}; then echo "Gateway ready after \${elapsed}s"; exit 0; fi`,
|
||||
` if ${portCheck}; then echo "Gateway ready after $elapsed sec"; exit 0; fi`,
|
||||
" printf '.'; sleep 1; elapsed=$((elapsed + 1))",
|
||||
"done",
|
||||
'echo "Gateway failed to start after 300s"; tail -20 /tmp/openclaw-gateway.log 2>/dev/null; exit 1',
|
||||
|
|
@ -645,6 +650,22 @@ const NPM_PREFIX_SETUP =
|
|||
// (e.g. Sprite VMs with flaky IPv6 routing to the npm registry)
|
||||
'export NODE_OPTIONS="${NODE_OPTIONS:-} --dns-result-order=ipv4first"';
|
||||
|
||||
/**
|
||||
* Validator-safe npm setup for base64-encoded helper scripts.
|
||||
*
|
||||
* setupAutoUpdate() rejects `${...}` inside encoded script templates, so the
|
||||
* auto-update path needs a shell snippet that avoids brace expansion while
|
||||
* still preserving the same prefix and PATH behavior as installs.
|
||||
*/
|
||||
const NPM_AUTO_UPDATE_SETUP =
|
||||
'_NPM_G_FLAGS=""; ' +
|
||||
'_npm_prefix="$(npm prefix -g 2>/dev/null || echo /usr/local)"; ' +
|
||||
'_npm_gbin="$_npm_prefix/bin"; ' +
|
||||
'if ! [ -w "$_npm_prefix" ] || ! printf "%s" ":$PATH:" | grep -qF ":$_npm_gbin:"; then ' +
|
||||
'mkdir -p "$HOME/.npm-global/bin"; _NPM_G_FLAGS="--prefix $HOME/.npm-global"; fi; ' +
|
||||
'export PATH="$HOME/.npm-global/bin:$PATH"; ' +
|
||||
'case " $NODE_OPTIONS " in *" --dns-result-order=ipv4first "*) ;; *) export NODE_OPTIONS="$NODE_OPTIONS --dns-result-order=ipv4first" ;; esac';
|
||||
|
||||
/**
|
||||
* Shell snippet that persists ~/.npm-global/bin in PATH across all shell config
|
||||
* files: ~/.bashrc, ~/.profile, ~/.bash_profile, and ~/.zshrc.
|
||||
|
|
@ -853,7 +874,7 @@ export async function setupAutoUpdate(runner: CloudRunner, agentName: string, up
|
|||
}
|
||||
|
||||
const script = [
|
||||
"if ! command -v systemctl >/dev/null 2>&1; then exit 0; fi",
|
||||
"if ! command -v systemctl >/dev/null 2>&1 || [ ! -d /run/systemd/system ]; then exit 0; fi",
|
||||
'_sudo=""',
|
||||
'[ "$(id -u)" != "0" ] && _sudo="sudo"',
|
||||
"printf '%s' '" + wrapperB64 + "' | base64 -d | $_sudo tee /usr/local/bin/spawn-auto-update > /dev/null",
|
||||
|
|
@ -869,7 +890,7 @@ export async function setupAutoUpdate(runner: CloudRunner, agentName: string, up
|
|||
|
||||
const result = await asyncTryCatch(() => runner.runServer(script));
|
||||
if (result.ok) {
|
||||
logInfo("Agent auto-update service installed (runs every 6 hours)");
|
||||
logInfo("Agent auto-update setup completed");
|
||||
} else {
|
||||
logWarn("Auto-update setup failed (non-fatal, agent still works)");
|
||||
}
|
||||
|
|
@ -916,9 +937,7 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
|
|||
],
|
||||
configure: () => setupCodexConfig(runner),
|
||||
launchCmd: () => "source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; codex",
|
||||
updateCmd:
|
||||
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
|
||||
"npm install -g ${_NPM_G_FLAGS:-} @openai/codex@latest",
|
||||
updateCmd: `${NPM_AUTO_UPDATE_SETUP} && ` + "npm install -g $_NPM_G_FLAGS @openai/codex@latest",
|
||||
},
|
||||
|
||||
openclaw: (() => {
|
||||
|
|
@ -950,9 +969,7 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
|
|||
remotePort: 18789,
|
||||
browserUrl: (localPort: number) => `http://localhost:${localPort}/#token=${dashboardToken}`,
|
||||
},
|
||||
updateCmd:
|
||||
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
|
||||
"npm install -g ${_NPM_G_FLAGS:-} openclaw@latest",
|
||||
updateCmd: `${NPM_AUTO_UPDATE_SETUP} && ` + "npm install -g $_NPM_G_FLAGS openclaw@latest",
|
||||
};
|
||||
})(),
|
||||
|
||||
|
|
@ -985,9 +1002,7 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
|
|||
`KILO_OPEN_ROUTER_API_KEY=${apiKey}`,
|
||||
],
|
||||
launchCmd: () => "source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; kilocode",
|
||||
updateCmd:
|
||||
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
|
||||
"npm install -g ${_NPM_G_FLAGS:-} @kilocode/cli@latest",
|
||||
updateCmd: `${NPM_AUTO_UPDATE_SETUP} && ` + "npm install -g $_NPM_G_FLAGS @kilocode/cli@latest",
|
||||
},
|
||||
|
||||
hermes: {
|
||||
|
|
@ -1044,9 +1059,7 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
|
|||
`OPENROUTER_API_KEY=${apiKey}`,
|
||||
],
|
||||
launchCmd: () => "source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; junie",
|
||||
updateCmd:
|
||||
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
|
||||
"npm install -g ${_NPM_G_FLAGS:-} @jetbrains/junie-cli@latest",
|
||||
updateCmd: `${NPM_AUTO_UPDATE_SETUP} && ` + "npm install -g $_NPM_G_FLAGS @jetbrains/junie-cli@latest",
|
||||
},
|
||||
|
||||
pi: {
|
||||
|
|
@ -1063,9 +1076,7 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
|
|||
`OPENROUTER_API_KEY=${apiKey}`,
|
||||
],
|
||||
launchCmd: () => "source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; pi",
|
||||
updateCmd:
|
||||
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
|
||||
"npm install -g ${_NPM_G_FLAGS:-} @mariozechner/pi-coding-agent@latest",
|
||||
updateCmd: `${NPM_AUTO_UPDATE_SETUP} && ` + "npm install -g $_NPM_G_FLAGS @mariozechner/pi-coding-agent@latest",
|
||||
},
|
||||
|
||||
cursor: {
|
||||
|
|
|
|||
|
|
@ -596,7 +596,11 @@ async function postInstall(
|
|||
|
||||
// Auto-update service
|
||||
if (cloud.cloudName !== "local" && agent.updateCmd && (!enabledSteps || enabledSteps.has("auto-update"))) {
|
||||
await setupAutoUpdate(cloud.runner, agentName, agent.updateCmd);
|
||||
if (cloud.cloudName === "daytona") {
|
||||
logInfo("Auto-update unavailable on Daytona — skipping");
|
||||
} else {
|
||||
await setupAutoUpdate(cloud.runner, agentName, agent.updateCmd);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn CLI + skill injection (recursive spawn)
|
||||
|
|
|
|||
|
|
@ -672,8 +672,10 @@ final_cleanup() {
|
|||
done
|
||||
fi
|
||||
if [ -n "${LOG_DIR:-}" ] && [ -d "${LOG_DIR:-}" ]; then
|
||||
SAFE_TMP_ROOT="${TMP_ROOT:-${TMPDIR:-/tmp}}"
|
||||
SAFE_TMP_ROOT="${SAFE_TMP_ROOT%/}"
|
||||
case "${LOG_DIR}" in
|
||||
/tmp/spawn-e2e.*)
|
||||
"${SAFE_TMP_ROOT}"/spawn-e2e.*)
|
||||
rm -rf "${LOG_DIR}"
|
||||
;;
|
||||
*)
|
||||
|
|
@ -708,7 +710,9 @@ fi
|
|||
export E2E_FAST_MODE="${FAST_MODE}"
|
||||
|
||||
# Create temp log directory
|
||||
LOG_DIR=$(mktemp -d "${TMPDIR:-/tmp}/spawn-e2e.XXXXXX")
|
||||
TMP_ROOT="${TMPDIR:-/tmp}"
|
||||
TMP_ROOT="${TMP_ROOT%/}"
|
||||
LOG_DIR=$(mktemp -d "${TMP_ROOT}/spawn-e2e.XXXXXX")
|
||||
export LOG_DIR
|
||||
log_info "Log directory: ${LOG_DIR}"
|
||||
|
||||
|
|
|
|||
|
|
@ -245,7 +245,9 @@ input_test_openclaw() {
|
|||
printf '%s' "${attempt}" | cloud_exec "${app}" "cat > /tmp/.e2e-attempt"
|
||||
|
||||
local output
|
||||
# The prompt, timeout, and attempt are read from staged temp files — no interpolation in this command.
|
||||
# Use plain-text output here. OpenClaw's JSON mode returns an envelope whose
|
||||
# payload may omit the final assistant text, while the plain-text mode emits
|
||||
# the reply body directly, which is what this marker test needs to assert.
|
||||
output=$(cloud_exec "${app}" "\
|
||||
source ~/.spawnrc 2>/dev/null; source ~/.bashrc 2>/dev/null; \
|
||||
export PATH=\$HOME/.npm-global/bin:\$HOME/.bun/bin:\$HOME/.local/bin:/usr/local/bin:\$PATH; \
|
||||
|
|
@ -253,7 +255,7 @@ input_test_openclaw() {
|
|||
_ATTEMPT=\$(cat /tmp/.e2e-attempt); \
|
||||
rm -rf /tmp/e2e-test && mkdir -p /tmp/e2e-test && cd /tmp/e2e-test && git init -q; \
|
||||
PROMPT=\$(cat /tmp/.e2e-prompt | base64 -d); \
|
||||
timeout \"\$_TIMEOUT\" openclaw agent --message \"\$PROMPT\" --session-id \"e2e-test-\$_ATTEMPT\" --json --timeout 60" 2>&1) || true
|
||||
timeout \"\$_TIMEOUT\" openclaw agent --message \"\$PROMPT\" --session-id \"e2e-test-\$_ATTEMPT\" --timeout 60" 2>&1) || true
|
||||
|
||||
if printf '%s' "${output}" | grep -qx "${INPUT_TEST_MARKER}"; then
|
||||
log_ok "openclaw input test — marker found in response"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue