Skip to content

Commit ea8f8cd

Browse files
authored
Merge 9bbc078 into 2ac083c
2 parents 2ac083c + 9bbc078 commit ea8f8cd

28 files changed

Lines changed: 4599 additions & 73 deletions

‎.github/workflows/smoke-nvx-copilot.lock.yml‎

Lines changed: 1807 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
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.

‎docs/awf-config-spec.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ AWF settings MAY be supplied via config files, including stdin (`--config -`).
340340
- `nvx.memoryMaxBytes` → `--nvx-memory-max-bytes` *(default 512 MiB)*
341341
- `nvx.pidsMax` → `--nvx-pids-max` *(default 128)*
342342
- `nvx.scratchBytes` → `--nvx-scratch-bytes`
343+
- `nvx.mountPolicy` → `--nvx-mount-policy` *(`"workspace-only"` (default) exports `$GITHUB_WORKSPACE` into the guest at `/workspace` read-write; `"workspace-and-tool-cache"` additionally exports the runner tool cache read-only. `filesystem.allowWrite` narrows the workspace export, and `container.containerWorkDir` must resolve inside `/workspace`.)*
343344
- `chroot.binariesSourcePath` → *(config-only; mounts a runner-side binaries directory at `/tmp/awf-runner-bin` inside chroot mode and prepends it to `PATH`)*
344345
- `chroot.identity.home` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_HOME` and applied after chroot pivot)*
345346
- `chroot.identity.user` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_USER` and applied to `USER`/`LOGNAME` after chroot pivot)*

‎docs/awf-config.schema.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,15 @@
10211021
"default": false,
10221022
"description": "Enable the NVX workload-execution preview. Requires container.containerRuntime: \"nvx\" plus a Linux x86_64 KVM host."
10231023
},
1024+
"mountPolicy": {
1025+
"type": "string",
1026+
"enum": [
1027+
"workspace-only",
1028+
"workspace-and-tool-cache"
1029+
],
1030+
"default": "workspace-only",
1031+
"description": "Host directory exposure policy for the live guest workspace export. \"workspace-only\" is the secure default and exports only the workspace read-write. \"workspace-and-tool-cache\" additionally requires and exports RUNNER_TOOL_CACHE or AGENT_TOOLSDIRECTORY read-only."
1032+
},
10241033
"layerPath": {
10251034
"type": "string",
10261035
"description": "Absolute path to the prebuilt guest distro layer directory used as the NVX filesystem base layer."

0 commit comments

Comments
 (0)