fix(security): harden shell scripts - fix sed portability, curl HTTPS enforcement, token expiry (#1917)

- MEDIUM: Validate flyctl auth status before empty FLY_API_TOKEN fallback
  in provision.sh (fail fast instead of silent failure)
- LOW: Fix sed -i portability in qa.sh (use sed -i.bak for macOS compat)
- LOW: Increase FLY_API_TOKEN expiry from 2h to 8h in common.sh
- LOW: Add --proto '=https' to all curl -L calls in digitalocean scripts
  (6 files) to prevent HTTP downgrade on redirects

Fixes #1913

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-25 03:23:32 -08:00 committed by GitHub
parent 9d7175bc1b
commit 4994c28594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 35 additions and 23 deletions

View file

@ -197,8 +197,9 @@ if [[ "${RUN_MODE}" == "quality" ]]; then
fi
cat "$PROMPT_TEMPLATE" > "${PROMPT_FILE}"
sed -i "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
sed -i.bak "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i.bak "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
rm -f "${PROMPT_FILE}.bak"
elif [[ "${RUN_MODE}" == "fixtures" ]]; then
PROMPT_TEMPLATE="${SCRIPT_DIR}/qa-fixtures-prompt.md"
@ -208,8 +209,9 @@ elif [[ "${RUN_MODE}" == "fixtures" ]]; then
fi
cat "$PROMPT_TEMPLATE" > "${PROMPT_FILE}"
sed -i "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
sed -i.bak "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i.bak "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
rm -f "${PROMPT_FILE}.bak"
elif [[ "${RUN_MODE}" == "issue" ]]; then
PROMPT_TEMPLATE="${SCRIPT_DIR}/qa-issue-prompt.md"
@ -219,9 +221,10 @@ elif [[ "${RUN_MODE}" == "issue" ]]; then
fi
cat "$PROMPT_TEMPLATE" > "${PROMPT_FILE}"
sed -i "s|ISSUE_NUM_PLACEHOLDER|${ISSUE_NUM}|g" "${PROMPT_FILE}"
sed -i "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
sed -i.bak "s|ISSUE_NUM_PLACEHOLDER|${ISSUE_NUM}|g" "${PROMPT_FILE}"
sed -i.bak "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i.bak "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
rm -f "${PROMPT_FILE}.bak"
elif [[ "${RUN_MODE}" == "e2e" ]]; then
PROMPT_TEMPLATE="${SCRIPT_DIR}/qa-e2e-prompt.md"
@ -231,8 +234,9 @@ elif [[ "${RUN_MODE}" == "e2e" ]]; then
fi
cat "$PROMPT_TEMPLATE" > "${PROMPT_FILE}"
sed -i "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
sed -i.bak "s|WORKTREE_BASE_PLACEHOLDER|${WORKTREE_BASE}|g" "${PROMPT_FILE}"
sed -i.bak "s|REPO_ROOT_PLACEHOLDER|${REPO_ROOT}|g" "${PROMPT_FILE}"
rm -f "${PROMPT_FILE}.bak"
fi

View file

@ -10,7 +10,7 @@ _MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
curl -fsSL --proto '=https' --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
@ -71,7 +71,7 @@ fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"

View file

@ -10,7 +10,7 @@ _MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
curl -fsSL --proto '=https' --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
@ -71,7 +71,7 @@ fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"

View file

@ -10,7 +10,7 @@ _MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
curl -fsSL --proto '=https' --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
@ -71,7 +71,7 @@ fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"

View file

@ -10,7 +10,7 @@ _MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
curl -fsSL --proto '=https' --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
@ -71,7 +71,7 @@ fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"

View file

@ -10,7 +10,7 @@ _MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
curl -fsSL --proto '=https' --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
@ -71,7 +71,7 @@ fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"

View file

@ -10,7 +10,7 @@ _MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
curl -fsSL --proto '=https' --show-error https://bun.sh/install | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
@ -71,7 +71,7 @@ fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"

View file

@ -83,7 +83,7 @@ require_env() {
# Check / generate FLY_API_TOKEN
if [ -z "${FLY_API_TOKEN:-}" ]; then
log_info "FLY_API_TOKEN not set, generating via flyctl..."
FLY_API_TOKEN=$(flyctl tokens create org personal --expiry 2h 2>/dev/null || true)
FLY_API_TOKEN=$(flyctl tokens create org personal --expiry 8h 2>/dev/null || true)
if [ -z "${FLY_API_TOKEN:-}" ]; then
log_warn "Could not generate token. Falling back to flyctl stored credentials."
# Validate flyctl is authenticated
@ -93,7 +93,7 @@ require_env() {
fi
else
export FLY_API_TOKEN
log_ok "Generated FLY_API_TOKEN (expires in 2h)"
log_ok "Generated FLY_API_TOKEN (expires in 8h)"
fi
fi

View file

@ -36,6 +36,14 @@ provision_agent() {
# Environment for headless provisioning
# FLY_API_TOKEN="" forces spawn to use flyctl stored credentials (see plan section 6)
# MODEL_ID bypasses the interactive model selection prompt (required by openclaw)
#
# Validate flyctl is authenticated before proceeding with empty token fallback
if [ -z "${FLY_API_TOKEN:-}" ]; then
if ! flyctl auth whoami >/dev/null 2>&1; then
log_err "FLY_API_TOKEN is empty and flyctl is not authenticated. Run: flyctl auth login"
return 1
fi
fi
(
SPAWN_NON_INTERACTIVE=1 \
SPAWN_SKIP_GITHUB_AUTH=1 \