mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-04 23:00:20 +00:00
* fix: pin all GitHub Actions to commit SHAs and version-lock tools Addresses supply chain hardening findings from issue #2982: - Pin all 6 GitHub Actions to full commit SHAs with version comments: - actions/checkout@v4 → SHA 34e1148... - oven-sh/setup-bun@v2 → SHA 0c5077e... - actions/github-script@v7 → SHA f28e40c... - docker/login-action@v3 → SHA c94ce9f... - docker/build-push-action@v6 → SHA 10e90e3... - hashicorp/setup-packer@main → SHA c3d53c5... (v3.2.0) - Pin Packer version: latest → 1.15.0 (in packer-snapshots.yml) - Pin bun version: latest → 1.3.11 (in agent-tarballs.yml) - Pin shellcheck: replace apt-get (no version) with pinned download of v0.10.0 from GitHub releases with SHA256 integrity check These changes eliminate the primary LiteLLM-style attack vector: a compromised action maintainer can no longer force-push malicious code to an existing tag and have it run in CI. Fixes #2982 Agent: issue-fixer Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: exclude import aliases from no-type-assertion lint rule The `JsNamedImportSpecifier` exclusion prevents `import { foo as bar }` patterns from being flagged as type assertions. Previously, any `as` keyword in import/export statements triggered the ban because the GritQL pattern `$value as $type` matched import specifiers as well as actual TypeScript type assertions. This also removes the `as _foo` import aliases in the script-failure-guidance test file (replaced with direct imports + distinctly-named wrapper functions) which were the original manifestation of this bug. All 1944 tests pass. Biome check clean across 169 files. Agent: issue-fixer Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
173 lines
6.8 KiB
YAML
173 lines
6.8 KiB
YAML
name: Agent Tarballs
|
|
|
|
on:
|
|
schedule:
|
|
# 5 AM UTC daily
|
|
- cron: "0 5 * * *"
|
|
workflow_dispatch:
|
|
inputs:
|
|
agent:
|
|
description: "Single agent to build (leave empty for all)"
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
matrix:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
agents: ${{ steps.set-matrix.outputs.agents }}
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
- id: set-matrix
|
|
env:
|
|
AGENT_INPUT: ${{ inputs.agent }}
|
|
run: |
|
|
if [ -n "${AGENT_INPUT:-}" ]; then
|
|
# Validate: agent name must be alphanumeric/hyphens only
|
|
if ! printf '%s' "${AGENT_INPUT}" | grep -qE '^[a-z][a-z0-9-]*$'; then
|
|
echo "::error::Invalid agent name: ${AGENT_INPUT}"
|
|
exit 1
|
|
fi
|
|
echo "agents=$(jq -cn --arg a "${AGENT_INPUT}" '[$a]')" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "agents=$(jq -c 'keys' packer/agents.json)" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
build:
|
|
needs: matrix
|
|
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
|
|
strategy:
|
|
max-parallel: 4
|
|
fail-fast: false
|
|
matrix:
|
|
agent: ${{ fromJson(needs.matrix.outputs.agents) }}
|
|
arch: [x86_64]
|
|
# Native-binary agents need ARM builds too.
|
|
# npm-based agents (codex, openclaw, kilocode) are arch-independent — x86_64 only.
|
|
include:
|
|
- agent: zeroclaw
|
|
arch: arm64
|
|
- agent: opencode
|
|
arch: arm64
|
|
- agent: hermes
|
|
arch: arm64
|
|
- agent: claude
|
|
arch: arm64
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
- name: Install Bun
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: "1.3.11"
|
|
|
|
- name: Install agent under /root
|
|
env:
|
|
AGENT_NAME: ${{ matrix.agent }}
|
|
run: |
|
|
set -eo pipefail
|
|
|
|
# Validate agent exists in agents.json (prevents path traversal / injection)
|
|
if ! jq -e --arg a "${AGENT_NAME}" 'has($a)' packer/agents.json > /dev/null; then
|
|
echo "::error::Unknown agent: ${AGENT_NAME}"
|
|
exit 1
|
|
fi
|
|
|
|
# Read the agent's tier from packer/agents.json
|
|
TIER=$(jq -r --arg a "${AGENT_NAME}" '.[$a].tier' packer/agents.json)
|
|
echo "==> Agent: ${AGENT_NAME}, Tier: ${TIER}"
|
|
|
|
# Run tier script (sets up node/bun/etc. under /root)
|
|
if [ -f "packer/scripts/tier-${TIER}.sh" ]; then
|
|
echo "==> Running tier script: tier-${TIER}.sh"
|
|
sudo HOME=/root bash "packer/scripts/tier-${TIER}.sh"
|
|
fi
|
|
|
|
# TRUST BOUNDARY: packer/agents.json is version-controlled and requires
|
|
# PR review to modify. Install commands are executed via bash -c, so any
|
|
# change to agents.json MUST be reviewed carefully for command safety.
|
|
#
|
|
# Security layers:
|
|
# 1. agents.json changes require PR review (branch protection)
|
|
# 2. curl/wget targets validated against domain allowlist
|
|
# 3. Suspicious command patterns rejected via blocklist
|
|
# 4. Runs in ephemeral GitHub Actions container (destroyed after each run)
|
|
echo "==> Installing agent..."
|
|
|
|
# Allowed domains for curl/wget downloads (official agent vendor domains)
|
|
ALLOWED_DOMAINS="claude.ai|opencode.ai|raw.githubusercontent.com|registry.npmjs.org|crates.io|github.com|dl.google.com"
|
|
|
|
CMD_COUNT=$(jq -r --arg a "${AGENT_NAME}" '.[$a].install | length' packer/agents.json)
|
|
i=0
|
|
while [ "$i" -lt "$CMD_COUNT" ]; do
|
|
cmd=$(jq -r --arg a "${AGENT_NAME}" --argjson i "$i" '.[$a].install[$i]' packer/agents.json)
|
|
|
|
# Safety layer 1: reject suspicious command patterns
|
|
if printf '%s' "${cmd}" | grep -qE '(mktemp|eval |base64 -d|/dev/tcp|nc -[elp]|python[23]? -c|perl -e|ruby -e|\bdd\b|>[[:space:]]*/dev/|rm -rf)'; then
|
|
echo "::error::Suspicious install command rejected: ${cmd}"
|
|
exit 1
|
|
fi
|
|
|
|
# Safety layer 2: validate curl/wget URLs against domain allowlist
|
|
if printf '%s' "${cmd}" | grep -qE '(curl|wget)'; then
|
|
urls=$(printf '%s' "${cmd}" | grep -oE 'https?://[^[:space:]"|'\'']+' || true)
|
|
for url in ${urls}; do
|
|
domain=$(printf '%s' "${url}" | sed -E 's|^https?://([^/]+).*|\1|')
|
|
if ! printf '%s' "${domain}" | grep -qE "^(${ALLOWED_DOMAINS})$"; then
|
|
echo "::error::curl/wget to unapproved domain '${domain}' in: ${cmd}"
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
echo "==> Running: ${cmd}"
|
|
sudo HOME=/root bash -c "${cmd}" < /dev/null
|
|
i=$((i + 1))
|
|
done
|
|
|
|
- name: Capture agent files into tarball
|
|
env:
|
|
AGENT_NAME: ${{ matrix.agent }}
|
|
run: |
|
|
set -eo pipefail
|
|
sudo bash packer/scripts/capture-agent.sh "${AGENT_NAME}"
|
|
|
|
- name: Create or update GitHub Release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
AGENT_NAME: ${{ matrix.agent }}
|
|
run: |
|
|
set -eo pipefail
|
|
TAG="agent-${AGENT_NAME}-latest"
|
|
DATE=$(date -u +%Y%m%d)
|
|
ARCH="${{ matrix.arch }}"
|
|
TARBALL="spawn-agent-${AGENT_NAME}-${ARCH}-${DATE}.tar.gz"
|
|
|
|
# Move tarball to expected name (tarball is owned by root from sudo capture)
|
|
sudo mv "/tmp/spawn-agent-${AGENT_NAME}.tar.gz" "${TARBALL}"
|
|
sudo chown "$(id -u):$(id -g)" "${TARBALL}"
|
|
|
|
# Create release if it doesn't exist, then upload the arch-specific tarball.
|
|
# Multiple arch builds (x86_64, arm64) upload to the same release.
|
|
if ! gh release view "${TAG}" > /dev/null 2>&1; then
|
|
gh release create "${TAG}" \
|
|
--title "Agent tarball: ${AGENT_NAME} (${DATE})" \
|
|
--notes "Pre-built tarball for \`${AGENT_NAME}\` agent. Auto-generated nightly." \
|
|
--prerelease
|
|
fi
|
|
|
|
# Delete stale asset for this arch if present (from a previous build today)
|
|
gh release delete-asset "${TAG}" "${TARBALL}" --yes 2>/dev/null || true
|
|
# Also clean up any older-dated tarball for this arch
|
|
# grep returns exit 1 when no matches — pipe through cat to avoid pipefail killing the step
|
|
gh release view "${TAG}" --json assets --jq ".assets[].name" 2>/dev/null \
|
|
| { grep "spawn-agent-${AGENT_NAME}-${ARCH}-" || true; } \
|
|
| while IFS= read -r old; do
|
|
gh release delete-asset "${TAG}" "${old}" --yes 2>/dev/null || true
|
|
done
|
|
|
|
gh release upload "${TAG}" "${TARBALL}"
|