Repository navigation
246 lines (230 loc) · 11 KB
/
Copy pathhub-e2e.yml
File metadata and controls
246 lines (230 loc) · 11 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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
name: Hub E2E
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: hub-e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
hub:
name: ${{ matrix.agent }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
agent:
- codex
- claude
- pi
- opencode
include:
- agent: codex
package: '@openai/codex@latest'
- agent: claude
package: '@anthropic-ai/claude-code@latest'
- agent: pi
package: '@earendil-works/pi-coding-agent@latest'
- agent: opencode
package: 'opencode-ai@latest'
env:
AGENT: ${{ matrix.agent }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
# Analysis only: capture uses tcpdump / the built-in Windows pktmon, not Npcap.
- name: Install network diagnostics (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update -qq
sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y tcpdump tshark
- name: Prepare network diagnostics (macOS)
if: runner.os == 'macOS'
run: |
curl --fail --location --retry 3 --output "$RUNNER_TEMP/wireshark.dmg" \
'https://www.wireshark.org/download/osx/all-versions/Wireshark%204.6.4.dmg'
echo "08150f79cfc5828820f991b6d944c68536db9595b1c3052982bbde79fb2053df $RUNNER_TEMP/wireshark.dmg" | shasum -a 256 --check
mount="$RUNNER_TEMP/wireshark-mount"
hdiutil attach "$RUNNER_TEMP/wireshark.dmg" -mountpoint "$mount" -readonly -nobrowse
trap 'hdiutil detach "$mount"' EXIT
ditto "$mount/Wireshark.app" "$RUNNER_TEMP/Wireshark.app"
echo "$RUNNER_TEMP/Wireshark.app/Contents/MacOS" >> "$GITHUB_PATH"
- name: Install network diagnostics (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$installer = Join-Path $env:RUNNER_TEMP 'wireshark.exe'
Invoke-WebRequest 'https://www.wireshark.org/download/win64/all-versions/Wireshark-4.6.4-x64.exe' -OutFile $installer
if ((Get-FileHash $installer -Algorithm SHA256).Hash -ne '102017d8e99a75b57895cd2144e6a61dc335a8ff14c7a25bd83a55f8ea9ad77b') {
throw 'Wireshark checksum mismatch'
}
# The silent installer does not install Npcap. Only offline analysis is needed.
$process = Start-Process $installer -ArgumentList '/S', '/desktopicon=no' -Wait -PassThru
if ($process.ExitCode -ne 0) { throw 'Wireshark installation failed' }
"$env:ProgramFiles\Wireshark" >> $env:GITHUB_PATH
- name: Build Wizard CLI
run: cargo build --locked --package wizard-cli
- name: Install agent
timeout-minutes: 10
run: npm install --global "${{ matrix.package }}"
- name: Configure Hub through Wizard CLI
timeout-minutes: 10
env:
COREINFRA_API_KEY: ${{ secrets.COREINFRA_API_KEY }}
run: |
if [ -z "$COREINFRA_API_KEY" ]; then
echo '::error::Set the COREINFRA_API_KEY repository secret.'
exit 1
fi
# Do not print settings, status, or package output: they may contain credentials.
printf '%s\n' "$COREINFRA_API_KEY" | cargo cli set-token > "$RUNNER_TEMP/wizard.log" 2>&1
unset COREINFRA_API_KEY
cargo cli "$AGENT" hub --yes >> "$RUNNER_TEMP/wizard.log" 2>&1
cargo cli status > "$RUNNER_TEMP/status.json" 2>> "$RUNNER_TEMP/wizard.log"
node -e '
const fs = require("node:fs");
const path = require("node:path");
const state = JSON.parse(fs.readFileSync(path.join(process.env.RUNNER_TEMP, "status.json"), "utf8"))[process.env.AGENT];
if (state?.status !== "found" || !(state.data.proxy_mode === "proxy_hub" || state.data.proxy_installed === true)) process.exit(1);
' 2>> "$RUNNER_TEMP/wizard.log"
- name: Start network capture
id: capture
run: |
umask 077
mkdir -p "$RUNNER_TEMP/network-capture"
cd "$RUNNER_TEMP/network-capture"
tshark --version > analyzer.log 2>&1
node -e '
const filters = Object.values(require("node:os").networkInterfaces()).flat()
.map(({ family, address }) => `${family === "IPv4" ? "ip" : "ipv6"}.src == ${address.split("%")[0]}`);
if (!filters.length) throw new Error("No local network addresses");
require("node:fs").writeFileSync("outgoing.filter", filters.join(" || "));
'
case "$RUNNER_OS" in
Windows)
pwsh -NoProfile -NonInteractive -Command '
pktmon start --capture --comp nics --pkt-size 0 --file-name capture.etl
if ($LASTEXITCODE -ne 0) { throw "Network capture failed to start" }
' > capture.log 2>&1
;;
Linux|macOS)
interface=any
if [ "$RUNNER_OS" = macOS ]; then interface=pktap,all; fi
# Save tcpdump's PID, not the sudo wrapper's. Drop privileges before writing packets.
sudo -n sh -c 'echo $$; exec tcpdump "$@"' sh \
-i "$interface" -n -s 0 -U -Z "$(id -un)" -w capture.pcapng > capture.pid 2> capture.log &
ready=false
for attempt in {1..100}; do
if grep -q 'listening on' capture.log; then ready=true; break; fi
sleep 0.1
done
if [ "$ready" != true ]; then
echo '::error::Network capture failed to start.'
exit 1
fi
;;
esac
- name: Ask agent through Hub
timeout-minutes: 5
env:
OPENCODE_PERMISSION: '{"*":"deny"}'
run: |
mkdir -p "$RUNNER_TEMP/hub-e2e/work"
cd "$RUNNER_TEMP/hub-e2e/work"
prompt='What is 17 + 25? Reply with exactly 42 and nothing else. Do not use tools.'
# The secret is not inherited here; authentication must come from Wizard-written files.
case "$AGENT" in
codex)
codex exec --skip-git-repo-check --sandbox read-only --ephemeral --model gpt-6-luna --output-last-message "$RUNNER_TEMP/answer.txt" "$prompt" > "$RUNNER_TEMP/agent.log" 2>&1
;;
claude)
claude --print --model claude-gpt-6-luna --tools "" --no-session-persistence "$prompt" > "$RUNNER_TEMP/answer.txt" 2> "$RUNNER_TEMP/agent.log"
;;
pi)
pi --print --model coreinfra/gpt-6-luna --no-tools --no-session "$prompt" > "$RUNNER_TEMP/answer.txt" 2> "$RUNNER_TEMP/agent.log"
;;
opencode)
opencode run --model coreinfra/gpt-6-luna --format json "$prompt" > "$RUNNER_TEMP/events.jsonl" 2> "$RUNNER_TEMP/agent.log"
node -e '
const fs = require("node:fs");
const path = require("node:path");
const events = fs.readFileSync(path.join(process.env.RUNNER_TEMP, "events.jsonl"), "utf8").trim().split(/\r?\n/).map(JSON.parse);
if (events.some(event => event.type === "error")) process.exit(1);
process.stdout.write(events.filter(event => event.type === "text").map(event => event.part.text).join(""));
' > "$RUNNER_TEMP/answer.txt" 2>> "$RUNNER_TEMP/agent.log"
;;
esac
if ! node -e 'process.exit(require("node:fs").readFileSync(process.argv[1], "utf8").trim() === "42" ? 0 : 1)' "$RUNNER_TEMP/answer.txt"; then
echo '::error::Expected the answer 42. Raw output is withheld because it may contain credentials.'
exit 1
fi
echo 'Hub request succeeded: 42'
# Also runs when the agent fails. Capture covers the whole runner, not just its processes.
- name: Stop capture and report network destinations
if: ${{ always() && (steps.capture.outcome == 'success' || steps.capture.outcome == 'failure') }}
timeout-minutes: 2
run: |
cd "$RUNNER_TEMP/network-capture"
# Never upload packet captures: unexpected plaintext traffic could contain credentials.
trap 'rm -f capture.pcapng capture.etl destinations.tsv' EXIT
case "$RUNNER_OS" in
Windows)
pwsh -NoProfile -NonInteractive -Command '
pktmon stop
if ($LASTEXITCODE -ne 0) { throw "Network capture failed to stop" }
pktmon etl2pcap capture.etl --out capture.pcapng
if ($LASTEXITCODE -ne 0) { throw "Network capture conversion failed" }
' > stop.log 2>&1
;;
Linux|macOS)
read -r pid < capture.pid
sudo -n kill -INT "$pid"
stopped=false
for attempt in {1..100}; do
if ! sudo -n kill -0 "$pid" 2>/dev/null; then stopped=true; break; fi
sleep 0.1
done
if [ "$stopped" != true ]; then
sudo -n kill -TERM "$pid" || true
echo '::error::Network capture did not stop cleanly.'
exit 1
fi
sudo -n chown "$(id -u):$(id -g)" capture.pcapng
chmod 600 capture.pcapng
# tcpdump reports counters only with -w; no packet contents are printed.
grep 'packets dropped by kernel' capture.log || true
if grep -Eq '[1-9][0-9]* packets dropped by kernel' capture.log; then
echo '::error::Network capture dropped packets.'
exit 1
fi
;;
esac
# -n prevents analysis from making reverse-DNS requests. Export metadata only.
tshark -n -2 -r capture.pcapng -Y "$(<outgoing.filter)" -T fields -E escape=y \
-e ip.dst -e ipv6.dst -e tcp.dstport -e udp.dstport \
-e dns.qry.name -e tls.handshake.extensions_server_name -e http.host \
> destinations.tsv 2> analyzer.log
if [ ! -s destinations.tsv ]; then
echo '::error::No outgoing IP packets observed; network audit is inconclusive.'
exit 1
fi
{
printf '## Network audit: %s (%s)\n\n' "$AGENT" "$RUNNER_OS"
printf 'Whole-runner traffic during the request; no process attribution or allowlist enforcement. Names may be absent. Windows: NIC capture only.\n\n'
printf '```text\nIPv4\tIPv6\tTCP port\tUDP port\tDNS name\tTLS SNI\tHTTP Host\n'
LC_ALL=C sort -u destinations.tsv
printf '```\n'
} | tee -a "$GITHUB_STEP_SUMMARY"