Skip to content

Commit 524acc5

Browse files
Merge branch 'james/sudoless-mac-vm'
2 parents 011e491 + 07b6b98 commit 524acc5

File tree

19 files changed

+6506
-20
lines changed

19 files changed

+6506
-20
lines changed

.github/workflows/release.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,75 @@ jobs:
149149
if [[ -n "${KEYCHAIN_PATH:-}" ]]; then
150150
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
151151
fi
152+
153+
publish-pinned-artifacts:
154+
name: Publish Pinned Base + Patch Metadata
155+
runs-on: macos-14
156+
needs: build-sign-release
157+
steps:
158+
- uses: actions/checkout@v4
159+
160+
- uses: dtolnay/rust-toolchain@stable
161+
162+
- uses: Swatinem/rust-cache@v2
163+
with:
164+
workspaces: crates
165+
166+
- name: Build vz-cli
167+
working-directory: crates
168+
run: cargo build --release -p vz-cli
169+
170+
- name: Validate pinned base matrix and patch bundles
171+
run: |
172+
set -euo pipefail
173+
174+
mkdir -p release/pinned-artifacts
175+
176+
cp config/base-images.json release/pinned-artifacts/base-images.json
177+
shasum -a 256 config/base-images.json > release/pinned-artifacts/base-images.json.sha256
178+
179+
crates/target/release/vz vm base list | tee release/pinned-artifacts/base-list.txt
180+
181+
bundle_count=0
182+
if [[ -d release/patch-bundles ]]; then
183+
shopt -s nullglob
184+
for bundle in release/patch-bundles/*.vzpatch; do
185+
if [[ -d "$bundle" ]]; then
186+
echo "Verifying patch bundle: $bundle"
187+
crates/target/release/vz vm patch verify --bundle "$bundle"
188+
bundle_count=$((bundle_count + 1))
189+
fi
190+
done
191+
shopt -u nullglob
192+
fi
193+
194+
if [[ -d release/patch-bundles ]]; then
195+
tar -czf release/pinned-artifacts/pinned-patch-bundles.tar.gz -C release patch-bundles
196+
shasum -a 256 release/pinned-artifacts/pinned-patch-bundles.tar.gz > release/pinned-artifacts/pinned-patch-bundles.tar.gz.sha256
197+
fi
198+
199+
generated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
200+
matrix_sha256="$(shasum -a 256 config/base-images.json | awk '{print $1}')"
201+
cat > release/pinned-artifacts/pinned-release-metadata.json <<EOF
202+
{
203+
"release_tag": "${GITHUB_REF_NAME}",
204+
"generated_at": "${generated_at}",
205+
"base_matrix_path": "config/base-images.json",
206+
"base_matrix_sha256": "${matrix_sha256}",
207+
"verified_patch_bundle_count": ${bundle_count}
208+
}
209+
EOF
210+
shasum -a 256 release/pinned-artifacts/pinned-release-metadata.json > release/pinned-artifacts/pinned-release-metadata.json.sha256
211+
212+
- name: Attach pinned artifacts to GitHub Release
213+
uses: softprops/action-gh-release@v2
214+
with:
215+
fail_on_unmatched_files: false
216+
files: |
217+
release/pinned-artifacts/base-images.json
218+
release/pinned-artifacts/base-images.json.sha256
219+
release/pinned-artifacts/base-list.txt
220+
release/pinned-artifacts/pinned-release-metadata.json
221+
release/pinned-artifacts/pinned-release-metadata.json.sha256
222+
release/pinned-artifacts/pinned-patch-bundles.tar.gz
223+
release/pinned-artifacts/pinned-patch-bundles.tar.gz.sha256

README.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,17 @@ vz stack down demo --volumes
9090
### 4. Manage macOS VMs (macOS only)
9191

9292
```bash
93-
# Create a base image from IPSW
94-
vz vm init --disk-size 64G
93+
# Create a pinned base image from the stable channel
94+
vz vm init --base stable
9595

96-
# Provision account + guest agent (one-time per image)
97-
sudo vz vm provision --image ~/.vz/images/base.img
96+
# Provision account + guest agent after fingerprint verification (system mode is default)
97+
sudo vz vm provision --image ~/.vz/images/base.img --base-id stable
98+
99+
# No-local-sudo local path (opt-in runtime policy)
100+
vz vm provision --image ~/.vz/images/base.img --base-id stable --agent-mode user
101+
102+
# Verify a local image against the stable channel pin
103+
vz vm base verify --image ~/.vz/images/base.img --base-id stable
98104

99105
# Start headless VM
100106
vz vm run --image ~/.vz/images/base.img --name dev --headless &
@@ -109,6 +115,64 @@ vz vm save dev --stop
109115
vz vm run --image ~/.vz/images/base.img --name dev --restore ~/.vz/state/dev.vzsave --headless &
110116
```
111117

118+
### 5. Pinned-base automation policy (macOS VM flows)
119+
120+
- `vz vm init --base <selector>`, `vz vm provision --base-id <selector>`, and `vz vm base verify --base-id <selector>` accept immutable base IDs plus channel aliases (`stable`, `previous`).
121+
- Base descriptors include support lifecycle metadata (`active` or `retired`); selecting a retired or unknown base fails with explicit fallback guidance.
122+
- Retirement guidance always includes `vz vm init --base stable` and, when available, a concrete replacement selector/base.
123+
- `vz vm patch verify` and `vz vm patch apply` reject bundles targeting retired or unsupported base descriptors.
124+
- Unpinned flows require explicit `--allow-unpinned`.
125+
- In CI (`CI=true`), unpinned flows are blocked unless `VZ_ALLOW_UNPINNED_IN_CI=1` is set.
126+
- Runtime policy: `--agent-mode system` is the default for reliability; `--agent-mode user` is opt-in for no-local-sudo workflows.
127+
128+
```bash
129+
# Explicit unpinned local flow
130+
vz vm init --allow-unpinned --ipsw ~/Downloads/restore.ipsw
131+
sudo vz vm provision --image ~/.vz/images/base.img --allow-unpinned
132+
```
133+
134+
### 6. Create signed patch bundles
135+
136+
```bash
137+
# Generate an Ed25519 signing key (PKCS#8 PEM)
138+
openssl genpkey -algorithm Ed25519 -out /tmp/vz-patch-signing-key.pem
139+
140+
# One-command inline patch creation (no operations.json or payload directory required)
141+
vz vm patch create \
142+
--bundle /tmp/patch-1.vzpatch \
143+
--base-id stable \
144+
--mkdir /usr/local/libexec:755 \
145+
--write-file /path/to/vz-agent:/usr/local/libexec/vz-agent:755 \
146+
--symlink /usr/local/bin/vz-agent:/usr/local/libexec/vz-agent \
147+
--set-owner /usr/local/libexec/vz-agent:0:0 \
148+
--set-mode /usr/local/libexec/vz-agent:755 \
149+
--signing-key /tmp/vz-patch-signing-key.pem
150+
151+
vz vm patch verify --bundle /tmp/patch-1.vzpatch
152+
sudo vz vm patch apply --bundle /tmp/patch-1.vzpatch --image ~/.vz/images/base.img
153+
```
154+
155+
For advanced CI workflows, `vz vm patch create` also supports `--operations <json>` + `--payload-dir <dir>`.
156+
157+
### 7. Primary image-delta patch flow (sudo once, then sudoless apply)
158+
159+
```bash
160+
# 1) Create a binary image delta from a signed bundle (runs bundle apply on a temp image copy)
161+
sudo vz vm patch create-delta \
162+
--bundle /tmp/patch-1.vzpatch \
163+
--base-image ~/.vz/images/base.img \
164+
--delta /tmp/patch-1.vzdelta
165+
166+
# 2) Apply the binary delta without sudo to produce a new bootable image
167+
vz vm patch apply-delta \
168+
--base-image ~/.vz/images/base.img \
169+
--delta /tmp/patch-1.vzdelta \
170+
--output-image ~/.vz/images/base-patched.img
171+
172+
# 3) Boot-test the patched image
173+
vz vm run --image ~/.vz/images/base-patched.img --name delta-test --headless
174+
```
175+
112176
## Command groups
113177

114178
### Containers
@@ -121,7 +185,7 @@ vz vm run --image ~/.vz/images/base.img --name dev --restore ~/.vz/state/dev.vzs
121185

122186
### VMs (macOS)
123187

124-
`vm init`, `vm run`, `vm exec`, `vm save`, `vm restore`, `vm list`, `vm stop`, `vm cache`, `vm provision`, `vm cleanup`, `vm self-sign`, `vm validate`
188+
`vm init`, `vm run`, `vm exec`, `vm save`, `vm restore`, `vm list`, `vm stop`, `vm cache`, `vm provision`, `vm cleanup`, `vm self-sign`, `vm validate`, `vm base`, `vm patch`
125189

126190
## Architecture
127191

config/base-images.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"version": 1,
3+
"default_base": "macos-15.3.1-24D70-arm64-64g",
4+
"channels": {
5+
"stable": "macos-15.3.1-24D70-arm64-64g",
6+
"previous": "macos-14.6-23G80-arm64-64g"
7+
},
8+
"bases": [
9+
{
10+
"base_id": "macos-15.3.1-24D70-arm64-64g",
11+
"macos_version": "15.3.1",
12+
"macos_build": "24D70",
13+
"ipsw_url": "https://updates.cdn-apple.com/2025WinterFCS/fullrestores/052-77521/UniversalMac_15.3.1_24D70_Restore.ipsw",
14+
"ipsw_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
15+
"disk_size_gb": 64,
16+
"fingerprint": {
17+
"img_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18+
"aux_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19+
"hwmodel_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
20+
"machineid_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
21+
},
22+
"support": {
23+
"status": "active"
24+
}
25+
},
26+
{
27+
"base_id": "macos-14.6-23G80-arm64-64g",
28+
"macos_version": "14.6",
29+
"macos_build": "23G80",
30+
"ipsw_url": "https://updates.cdn-apple.com/2024SummerFCS/fullrestores/042-15372/UniversalMac_14.6_23G80_Restore.ipsw",
31+
"ipsw_sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
32+
"disk_size_gb": 64,
33+
"fingerprint": {
34+
"img_sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
35+
"aux_sha256": "1111111111111111111111111111111111111111111111111111111111111111",
36+
"hwmodel_sha256": "2222222222222222222222222222222222222222222222222222222222222222",
37+
"machineid_sha256": "3333333333333333333333333333333333333333333333333333333333333333"
38+
},
39+
"support": {
40+
"status": "active"
41+
}
42+
},
43+
{
44+
"base_id": "macos-13.6.7-22H123-arm64-64g",
45+
"macos_version": "13.6.7",
46+
"macos_build": "22H123",
47+
"ipsw_url": "https://updates.cdn-apple.com/2024SpringFCS/fullrestores/032-12345/UniversalMac_13.6.7_22H123_Restore.ipsw",
48+
"ipsw_sha256": "4444444444444444444444444444444444444444444444444444444444444444",
49+
"disk_size_gb": 64,
50+
"fingerprint": {
51+
"img_sha256": "5555555555555555555555555555555555555555555555555555555555555555",
52+
"aux_sha256": "6666666666666666666666666666666666666666666666666666666666666666",
53+
"hwmodel_sha256": "7777777777777777777777777777777777777777777777777777777777777777",
54+
"machineid_sha256": "8888888888888888888888888888888888888888888888888888888888888888"
55+
},
56+
"support": {
57+
"status": "retired",
58+
"retired_at": "2026-01-31",
59+
"replacement_selector": "previous",
60+
"replacement_base_id": "macos-14.6-23G80-arm64-64g"
61+
}
62+
}
63+
]
64+
}

0 commit comments

Comments
 (0)