-
Notifications
You must be signed in to change notification settings - Fork 19
173 lines (151 loc) · 6.81 KB
/
agent-tarballs.yml
File metadata and controls
173 lines (151 loc) · 6.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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: opencode
arch: arm64
- agent: hermes
arch: arm64
- agent: claude
arch: arm64
- agent: cursor
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|cursor.com|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}"