|
| 1 | +--- |
| 2 | +description: Smoke test the NVX microVM runtime with a real Copilot agent |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + label_command: |
| 6 | + name: test-nvx-copilot |
| 7 | + events: [pull_request] |
| 8 | + remove_label: false |
| 9 | + reaction: "eyes" |
| 10 | +concurrency: |
| 11 | + job-discriminator: ${{ github.run_id }} |
| 12 | +permissions: |
| 13 | + contents: read |
| 14 | + pull-requests: read |
| 15 | + issues: read |
| 16 | + actions: read |
| 17 | + copilot-requests: write |
| 18 | +name: Smoke NVX Copilot |
| 19 | +engine: |
| 20 | + id: copilot |
| 21 | +network: |
| 22 | + allowed: |
| 23 | + - defaults |
| 24 | + - github |
| 25 | +tools: |
| 26 | + bash: |
| 27 | + - "*" |
| 28 | + github: |
| 29 | + toolsets: [pull_requests] |
| 30 | +safe-outputs: |
| 31 | + threat-detection: |
| 32 | + enabled: false |
| 33 | + add-comment: |
| 34 | + hide-older-comments: true |
| 35 | + add-labels: |
| 36 | + allowed: [smoke-nvx-copilot] |
| 37 | + noop: |
| 38 | + messages: |
| 39 | + footer: "> NVX + Copilot smoke test by [{workflow_name}]({run_url})" |
| 40 | + run-started: "[{workflow_name}]({run_url}) is testing the NVX microVM runtime with Copilot..." |
| 41 | + run-success: "[{workflow_name}]({run_url}) completed. NVX + Copilot passed." |
| 42 | + run-failure: "[{workflow_name}]({run_url}) reports {status}. NVX + Copilot failed." |
| 43 | +timeout-minutes: 45 |
| 44 | +strict: false |
| 45 | +jobs: |
| 46 | + build_nvx_artifacts: |
| 47 | + name: Build workflow-attested NVX artifacts |
| 48 | + runs-on: ubuntu-24.04 |
| 49 | + timeout-minutes: 20 |
| 50 | + permissions: |
| 51 | + contents: read |
| 52 | + id-token: write |
| 53 | + attestations: write |
| 54 | + steps: |
| 55 | + - name: Checkout repository |
| 56 | + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 |
| 57 | + with: |
| 58 | + persist-credentials: false |
| 59 | + - name: Fetch and verify pinned NVX release artifacts |
| 60 | + env: |
| 61 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 62 | + NVX_RELEASE: v0.1.0-dev.d561c4300ebe |
| 63 | + NVX_ARCHIVE: nvx-0.1.0-linux-kvm.tar.gz |
| 64 | + NVX_ARCHIVE_SHA256: 705c863cf7183e89606542b12961644eefd24fed8b2520156dd5cb63a3982699 |
| 65 | + SIGNER_WORKFLOW: github/gh-aw-firewall/.github/workflows/smoke-nvx-copilot.lock.yml |
| 66 | + run: | |
| 67 | + set -euo pipefail |
| 68 | + package_dir="$RUNNER_TEMP/nvx-package" |
| 69 | + artifact_dir="$RUNNER_TEMP/nvx-attested-artifacts" |
| 70 | + mkdir -p "$package_dir" "$artifact_dir" |
| 71 | + gh release download "$NVX_RELEASE" \ |
| 72 | + --repo microsoft/nvx \ |
| 73 | + --pattern "$NVX_ARCHIVE" \ |
| 74 | + --dir "$package_dir" |
| 75 | + printf '%s %s\n' "$NVX_ARCHIVE_SHA256" "$package_dir/$NVX_ARCHIVE" | |
| 76 | + sha256sum --check --status |
| 77 | + tar -xzf "$package_dir/$NVX_ARCHIVE" -C "$package_dir" |
| 78 | + extracted="$package_dir/nvx-0.1.0-linux-kvm" |
| 79 | + (cd "$extracted" && sha256sum --check SHA256SUMS) |
| 80 | + install -m 0555 "$extracted/bin/openvmm" "$artifact_dir/openvmm" |
| 81 | + install -m 0444 "$extracted/guest/vmlinux" "$artifact_dir/vmlinux" |
| 82 | + install -m 0444 "$extracted/guest/initramfs.cpio.gz" \ |
| 83 | + "$artifact_dir/initramfs.cpio.gz" |
| 84 | + release_tag="v$(node -p "require('./package.json').version")" |
| 85 | + jq -n \ |
| 86 | + --arg release_tag "$release_tag" \ |
| 87 | + --arg source_commit "$GITHUB_SHA" \ |
| 88 | + --arg signer_workflow "$SIGNER_WORKFLOW" \ |
| 89 | + --arg openvmm_sha "$(sha256sum "$artifact_dir/openvmm" | cut -d' ' -f1)" \ |
| 90 | + --arg kernel_sha "$(sha256sum "$artifact_dir/vmlinux" | cut -d' ' -f1)" \ |
| 91 | + --arg initramfs_sha "$(sha256sum "$artifact_dir/initramfs.cpio.gz" | cut -d' ' -f1)" \ |
| 92 | + --argjson openvmm_size "$(stat -c %s "$artifact_dir/openvmm")" \ |
| 93 | + --argjson kernel_size "$(stat -c %s "$artifact_dir/vmlinux")" \ |
| 94 | + --argjson initramfs_size "$(stat -c %s "$artifact_dir/initramfs.cpio.gz")" \ |
| 95 | + '{ |
| 96 | + schemaVersion:2, |
| 97 | + release:{ |
| 98 | + repository:"github/gh-aw-firewall", |
| 99 | + workflow:$signer_workflow, |
| 100 | + tag:$release_tag, |
| 101 | + sourceCommit:$source_commit |
| 102 | + }, |
| 103 | + upstream:{ |
| 104 | + releaseTag:"v0.1.0-dev.d561c4300ebe", |
| 105 | + nvxCommit:"d561c4300ebe854baba5d154056ead6f9d462047", |
| 106 | + openvmmCommit:"0bc357bbcf3a654b63dfb51f1103c5751bf3d31f" |
| 107 | + }, |
| 108 | + architecture:"x86_64", |
| 109 | + artifacts:{ |
| 110 | + openvmm:{file:"openvmm",sizeBytes:$openvmm_size,sha256:$openvmm_sha}, |
| 111 | + kernel:{file:"vmlinux",sizeBytes:$kernel_size,sha256:$kernel_sha}, |
| 112 | + initramfs:{ |
| 113 | + file:"initramfs.cpio.gz", |
| 114 | + sizeBytes:$initramfs_size, |
| 115 | + sha256:$initramfs_sha |
| 116 | + } |
| 117 | + } |
| 118 | + }' > "$artifact_dir/manifest.json" |
| 119 | + - name: Attest the pinned NVX artifact manifest |
| 120 | + id: attest_nvx_manifest |
| 121 | + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 |
| 122 | + with: |
| 123 | + subject-path: ${{ runner.temp }}/nvx-attested-artifacts/manifest.json |
| 124 | + - name: Bundle the manifest attestation |
| 125 | + env: |
| 126 | + BUNDLE_PATH: ${{ steps.attest_nvx_manifest.outputs.bundle-path }} |
| 127 | + run: | |
| 128 | + set -euo pipefail |
| 129 | + cp "$BUNDLE_PATH" "$RUNNER_TEMP/nvx-attested-artifacts/manifest.sigstore.jsonl" |
| 130 | + - name: Upload attested NVX artifacts |
| 131 | + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 |
| 132 | + with: |
| 133 | + name: nvx-smoke-attested-artifacts |
| 134 | + path: ${{ runner.temp }}/nvx-attested-artifacts/ |
| 135 | + if-no-files-found: error |
| 136 | + retention-days: 1 |
| 137 | +steps: |
| 138 | + - name: Set up Node.js |
| 139 | + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 |
| 140 | + with: |
| 141 | + node-version: '22' |
| 142 | + cache: npm |
| 143 | + |
| 144 | + - name: Install host tools and build AWF |
| 145 | + run: | |
| 146 | + set -euo pipefail |
| 147 | + sudo apt-get update |
| 148 | + sudo apt-get install --yes --no-install-recommends \ |
| 149 | + acl bubblewrap e2fsprogs erofs-utils jq nftables uidmap |
| 150 | + npm ci |
| 151 | + npm run build |
| 152 | +
|
| 153 | + - name: Download the attested NVX artifacts |
| 154 | + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 |
| 155 | + with: |
| 156 | + name: nvx-smoke-attested-artifacts |
| 157 | + path: ${{ runner.temp }}/nvx-attested-artifacts |
| 158 | + |
| 159 | + - name: Restore artifact permissions |
| 160 | + run: | |
| 161 | + set -euo pipefail |
| 162 | + artifact_dir="$RUNNER_TEMP/nvx-attested-artifacts" |
| 163 | + chmod 0555 "$artifact_dir/openvmm" |
| 164 | + chmod 0444 "$artifact_dir/vmlinux" "$artifact_dir/initramfs.cpio.gz" \ |
| 165 | + "$artifact_dir/manifest.json" "$artifact_dir/manifest.sigstore.jsonl" |
| 166 | +
|
| 167 | + - name: Build the guest distro layer with the pinned Copilot CLI |
| 168 | + env: |
| 169 | + ALPINE_IMAGE: alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f |
| 170 | + COPILOT_PACKAGE: '@github/copilot-linuxmusl-x64' |
| 171 | + COPILOT_VERSION: 1.0.86 |
| 172 | + COPILOT_ARCHIVE: github-copilot-linuxmusl-x64-1.0.86.tgz |
| 173 | + COPILOT_ARCHIVE_SHA256: 34cf74e32c5227efd1957ff69f9ad957205968621c1ce4a84384ece6c19a2f6c |
| 174 | + run: | |
| 175 | + set -euo pipefail |
| 176 | + layer_root="$RUNNER_TEMP/nvx-alpine-root" |
| 177 | + package_dir="$RUNNER_TEMP/nvx-copilot-package" |
| 178 | + mkdir -p "$layer_root" "$package_dir" |
| 179 | + sudo docker pull "$ALPINE_IMAGE" |
| 180 | + container=$(sudo docker create "$ALPINE_IMAGE") |
| 181 | + sudo docker export "$container" | tar -xf - -C "$layer_root" |
| 182 | + sudo docker rm "$container" |
| 183 | + mkdir -p "$layer_root/etc" "$layer_root/usr/local/bin" |
| 184 | + printf 'runner:x:%s:%s:runner:/home/awf:/bin/sh\n' "$(id -u)" "$(id -g)" \ |
| 185 | + > "$layer_root/etc/passwd" |
| 186 | + printf 'runner:x:%s:\n' "$(id -g)" > "$layer_root/etc/group" |
| 187 | + chmod 1777 "$layer_root/tmp" |
| 188 | +
|
| 189 | + npm pack "${COPILOT_PACKAGE}@${COPILOT_VERSION}" \ |
| 190 | + --pack-destination "$RUNNER_TEMP" --silent |
| 191 | + printf '%s %s\n' "$COPILOT_ARCHIVE_SHA256" "$RUNNER_TEMP/$COPILOT_ARCHIVE" | |
| 192 | + sha256sum --check --status |
| 193 | + tar -xzf "$RUNNER_TEMP/$COPILOT_ARCHIVE" --strip-components=1 -C "$package_dir" |
| 194 | + install -m 0755 "$package_dir/copilot" "$layer_root/usr/local/bin/copilot" |
| 195 | +
|
| 196 | + # The guest command exercises every capability this smoke test is about: |
| 197 | + # the live workspace export, --container-workdir, per-run environment |
| 198 | + # passthrough, and a real coding agent writing a workspace file that must |
| 199 | + # be copied back to the host. |
| 200 | + cat > "$layer_root/usr/local/bin/awf-nvx-smoke" <<'EOF' |
| 201 | + #!/bin/sh |
| 202 | + set -eu |
| 203 | + test "$(pwd)" = /workspace |
| 204 | + test -r /workspace/package.json |
| 205 | + test -n "${AWF_NVX_SMOKE_MARKER:-}" |
| 206 | + test -n "${HTTPS_PROXY:-}" |
| 207 | + if env | grep -Eq '^(GH_TOKEN|GITHUB_TOKEN|COPILOT_GITHUB_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY)='; then |
| 208 | + echo "credential variable reached the NVX guest" >&2 |
| 209 | + exit 1 |
| 210 | + fi |
| 211 | + printf '%s\n' "$AWF_NVX_SMOKE_MARKER" > /workspace/nvx-smoke-workspace-proof.txt |
| 212 | + runtime_home=/tmp/copilot-home |
| 213 | + mkdir -m 0700 "$runtime_home" |
| 214 | + HOME="$runtime_home"; export HOME |
| 215 | + XDG_CACHE_HOME="$runtime_home/.cache"; export XDG_CACHE_HOME |
| 216 | + XDG_CONFIG_HOME="$runtime_home/.config"; export XDG_CONFIG_HOME |
| 217 | + XDG_STATE_HOME="$runtime_home/.local/state"; export XDG_STATE_HOME |
| 218 | + /usr/local/bin/copilot \ |
| 219 | + --prompt "Respond with exactly NVX-COPILOT-PROOF and nothing else." \ |
| 220 | + --silent --no-color --stream on --allow-all-tools \ |
| 221 | + --disable-builtin-mcps --no-custom-instructions --no-auto-update \ |
| 222 | + --no-ask-user --model claude-sonnet-5 --max-ai-credits 30 \ |
| 223 | + > /workspace/nvx-smoke-copilot-proof.txt |
| 224 | + echo AWF-NVX-SMOKE-COMPLETE |
| 225 | + EOF |
| 226 | + chmod 0755 "$layer_root/usr/local/bin/awf-nvx-smoke" |
| 227 | +
|
| 228 | + - name: Run the Copilot agent inside an NVX microVM |
| 229 | + env: |
| 230 | + COPILOT_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 231 | + run: | |
| 232 | + # Evidence-producing: a failed scenario must be recorded for the agent to |
| 233 | + # analyze rather than aborting the step before the summary is written. |
| 234 | + set +e |
| 235 | + set -u |
| 236 | + data_dir=/tmp/gh-aw/agent/smoke-nvx-copilot |
| 237 | + mkdir -p "$data_dir/logs" |
| 238 | + results="$data_dir/scenarios.jsonl" |
| 239 | + : > "$results" |
| 240 | +
|
| 241 | + record() { |
| 242 | + jq -cn --arg check "$1" --arg status "$2" --arg detail "$3" \ |
| 243 | + '{check:$check,status:$status,detail:$detail}' >> "$results" |
| 244 | + } |
| 245 | +
|
| 246 | + artifact_dir="$RUNNER_TEMP/nvx-attested-artifacts" |
| 247 | + layer_root="$RUNNER_TEMP/nvx-alpine-root" |
| 248 | + marker="nvx-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" |
| 249 | + proof_file="$GITHUB_WORKSPACE/nvx-smoke-workspace-proof.txt" |
| 250 | + copilot_file="$GITHUB_WORKSPACE/nvx-smoke-copilot-proof.txt" |
| 251 | + rm -f "$proof_file" "$copilot_file" |
| 252 | +
|
| 253 | + sudo --preserve-env=COPILOT_GITHUB_TOKEN \ |
| 254 | + AWF_NVX_SMOKE_MARKER="$marker" \ |
| 255 | + node "$GITHUB_WORKSPACE/dist/cli.js" \ |
| 256 | + --container-runtime nvx \ |
| 257 | + --nvx-preview \ |
| 258 | + --nvx-layer "$layer_root" \ |
| 259 | + --nvx-openvmm "$artifact_dir/openvmm" \ |
| 260 | + --nvx-kernel "$artifact_dir/vmlinux" \ |
| 261 | + --nvx-initramfs "$artifact_dir/initramfs.cpio.gz" \ |
| 262 | + --nvx-artifact-manifest "$artifact_dir/manifest.json" \ |
| 263 | + --nvx-artifact-manifest-bundle "$artifact_dir/manifest.sigstore.jsonl" \ |
| 264 | + --nvx-signer-workflow \ |
| 265 | + 'github/gh-aw-firewall/.github/workflows/smoke-nvx-copilot.lock.yml' \ |
| 266 | + --nvx-mount-policy workspace-only \ |
| 267 | + --container-workdir /workspace \ |
| 268 | + --network-isolation \ |
| 269 | + --enable-api-proxy \ |
| 270 | + --allow-domains github.com,api.github.com,api.githubcopilot.com \ |
| 271 | + --env AWF_NVX_SMOKE_MARKER \ |
| 272 | + --log-level info \ |
| 273 | + -- /usr/local/bin/awf-nvx-smoke \ |
| 274 | + > "$data_dir/logs/awf.log" 2>&1 |
| 275 | + awf_status=$? |
| 276 | +
|
| 277 | + if [ "$awf_status" -eq 0 ]; then |
| 278 | + record microvm-run PASS "awf exited 0" |
| 279 | + else |
| 280 | + record microvm-run FAIL "awf exited $awf_status; see logs/awf.log" |
| 281 | + fi |
| 282 | +
|
| 283 | + if grep -q 'AWF-NVX-SMOKE-COMPLETE' "$data_dir/logs/awf.log"; then |
| 284 | + record guest-assertions PASS \ |
| 285 | + "guest confirmed /workspace export, --container-workdir, and env passthrough" |
| 286 | + else |
| 287 | + record guest-assertions FAIL \ |
| 288 | + "guest did not reach AWF-NVX-SMOKE-COMPLETE" |
| 289 | + fi |
| 290 | +
|
| 291 | + if [ -f "$proof_file" ] && [ "$(cat "$proof_file")" = "$marker" ]; then |
| 292 | + record workspace-copy-back PASS "guest write reached the host workspace" |
| 293 | + else |
| 294 | + record workspace-copy-back FAIL \ |
| 295 | + "expected $marker in nvx-smoke-workspace-proof.txt" |
| 296 | + fi |
| 297 | +
|
| 298 | + if grep -q 'NVX-COPILOT-PROOF' "$copilot_file" 2>/dev/null; then |
| 299 | + record copilot-inference PASS "Copilot responded through the API proxy" |
| 300 | + else |
| 301 | + record copilot-inference FAIL \ |
| 302 | + "no NVX-COPILOT-PROOF in nvx-smoke-copilot-proof.txt" |
| 303 | + fi |
| 304 | +
|
| 305 | + rm -f "$proof_file" "$copilot_file" |
| 306 | + sudo chown -R "$(id -u):$(id -g)" "$data_dir" |
| 307 | + cat "$results" |
| 308 | + exit 0 |
| 309 | +post-steps: |
| 310 | + - name: Validate safe outputs were invoked |
| 311 | + run: | |
| 312 | + OUTPUTS_FILE="${GH_AW_SAFE_OUTPUTS:-${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl}" |
| 313 | + if [ ! -s "$OUTPUTS_FILE" ]; then |
| 314 | + echo "::error::No safe outputs were invoked." |
| 315 | + exit 1 |
| 316 | + fi |
| 317 | +--- |
| 318 | + |
| 319 | +> **[ASSIGNED WORKFLOW TASK]** Execute the authorized smoke test below. |
| 320 | +
|
| 321 | +# Smoke Test: NVX microVM + Copilot |
| 322 | + |
| 323 | +A pre-agent step already ran the pinned Copilot CLI as a real coding agent |
| 324 | +inside an NVX one-shot microVM, through AWF's `nvx` runtime, and recorded one |
| 325 | +JSON object per check. |
| 326 | + |
| 327 | +1. Read `/tmp/gh-aw/agent/smoke-nvx-copilot/scenarios.jsonl`. |
| 328 | +2. Report a PASS or FAIL line for each of `microvm-run`, `guest-assertions`, |
| 329 | + `workspace-copy-back`, and `copilot-inference`. |
| 330 | +3. If anything failed, read `/tmp/gh-aw/agent/smoke-nvx-copilot/logs/awf.log` |
| 331 | + and add one short line naming the most likely cause. |
| 332 | + |
| 333 | +Do not re-run the microVM yourself; only analyze the recorded evidence. |
| 334 | + |
| 335 | +Keep the summary under 10 lines. |
| 336 | + |
| 337 | +On a pull request trigger, call `add_comment` with `item_number: ${{ github.event.pull_request.number }}`. If all checks pass, call `add_labels` with the same item number and label `smoke-nvx-copilot`. |
| 338 | + |
| 339 | +On `workflow_dispatch`, call `noop` with the concise summary instead. |
0 commit comments