fix(daytona): tighten live provider behavior

This commit is contained in:
Muhammad Hashmi 2026-04-02 19:11:40 -07:00
parent a7be238ef4
commit c9b663d371
6 changed files with 88 additions and 55 deletions

View file

@ -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) {

View file

@ -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;
}
/**

View file

@ -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: {

View file

@ -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)

View file

@ -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}"

View file

@ -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"