fix: use correct linux VM checksums in cowork manifest patch (#329)

The cowork manifest patch (Patch 4) copied win32 file entries as linux
entries. Since Anthropic now publishes Linux-specific VM images with
different content, the win32 checksums cause silent validation failures
and startVM timeouts.

Compute correct SHA-256 checksums for the linux CDN files and embed
them in build.sh. Patch 4 now replaces win32 checksums with the linux
values before injecting the manifest entry. Falls back to win32 values
if linux checksums are empty.

The check-claude-version CI workflow is extended to automatically
recompute VM checksums when a new version is detected. This is
non-blocking — if CDN files aren't published yet or computation fails,
the rest of the workflow proceeds unaffected.

Fixes #329

Co-Authored-By: Claude <claude@anthropic.com>
This commit is contained in:
aaddrick
2026-03-22 08:26:45 -04:00
parent bc1074e70c
commit aa6b87dc52
2 changed files with 216 additions and 23 deletions

View File

@@ -30,7 +30,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y p7zip-full wget
sudo apt-get install -y p7zip-full wget zstd
pip install playwright requests
playwright install chromium
@@ -171,11 +171,12 @@ jobs:
# Download to temp files first so a partial download doesn't
# produce a valid-looking but wrong SRI hash.
# Keep the AMD64 installer for VM checksum extraction later.
amd64_tmp=$(mktemp)
curl -fsSL -o "$amd64_tmp" "$AMD64_URL" || { echo "AMD64 download failed"; exit 1; }
AMD64_HEX=$(sha256sum "$amd64_tmp" | awk '{print $1}')
AMD64_HASH=$(echo "$AMD64_HEX" | xxd -r -p | base64 -w0)
rm "$amd64_tmp"
echo "amd64_installer=$amd64_tmp" >> $GITHUB_OUTPUT
echo "amd64_sri=sha256-$AMD64_HASH" >> $GITHUB_OUTPUT
echo "amd64_sha256=$AMD64_HEX" >> $GITHUB_OUTPUT
@@ -214,6 +215,141 @@ jobs:
echo "Updated build.sh checksums:"
grep "claude_exe_sha256=" build.sh
- name: Extract VM bundle SHA from installer
if: steps.check_update.outputs.update_needed == 'true'
id: vm_bundle
run: |
INSTALLER="${{ steps.nix_hashes.outputs.amd64_installer }}"
if [ -z "$INSTALLER" ] || [ ! -f "$INSTALLER" ]; then
echo "No installer available, skipping VM checksums"
echo "bundle_sha=" >> $GITHUB_OUTPUT
exit 0
fi
WORK=$(mktemp -d)
echo "Extracting installer to find bundle SHA..."
# Extract exe → nupkg → app.asar → index.js
7z x -y -o"$WORK/exe" "$INSTALLER" > /dev/null 2>&1
NUPKG=$(find "$WORK/exe" -name "AnthropicClaude-*.nupkg" | head -1)
if [ -z "$NUPKG" ]; then
echo "No nupkg found, skipping VM checksums"
rm -rf "$WORK"
echo "bundle_sha=" >> $GITHUB_OUTPUT
exit 0
fi
7z x -y -o"$WORK/nupkg" "$NUPKG" > /dev/null 2>&1
ASAR="$WORK/nupkg/lib/net45/resources/app.asar"
if [ ! -f "$ASAR" ]; then
echo "No app.asar found, skipping VM checksums"
rm -rf "$WORK"
echo "bundle_sha=" >> $GITHUB_OUTPUT
exit 0
fi
npx --yes @electron/asar extract "$ASAR" "$WORK/app" 2>/dev/null
INDEX_JS="$WORK/app/.vite/build/index.js"
if [ ! -f "$INDEX_JS" ]; then
echo "No index.js found, skipping VM checksums"
rm -rf "$WORK"
echo "bundle_sha=" >> $GITHUB_OUTPUT
exit 0
fi
# Extract bundle SHA (40-char hex after sha: or sha:")
BUNDLE_SHA=$(grep -oP 'sha\s*:\s*"([a-f0-9]{40})"' "$INDEX_JS" \
| grep -oP '[a-f0-9]{40}' | head -1)
rm -rf "$WORK"
rm -f "$INSTALLER"
if [ -z "$BUNDLE_SHA" ]; then
echo "Could not extract bundle SHA, skipping VM checksums"
echo "bundle_sha=" >> $GITHUB_OUTPUT
exit 0
fi
echo "Bundle SHA: $BUNDLE_SHA"
# Validate CDN has linux files for this SHA
HTTP_CODE=$(curl -sI -o /dev/null -w "%{http_code}" \
"https://downloads.claude.ai/vms/linux/x64/$BUNDLE_SHA/vmlinuz.zst")
if [ "$HTTP_CODE" != "200" ]; then
echo "Linux VM files not published yet (HTTP $HTTP_CODE), skipping"
echo "bundle_sha=" >> $GITHUB_OUTPUT
exit 0
fi
echo "Linux VM files confirmed on CDN"
echo "bundle_sha=$BUNDLE_SHA" >> $GITHUB_OUTPUT
- name: Compute VM bundle checksums
if: steps.check_update.outputs.update_needed == 'true' && steps.vm_bundle.outputs.bundle_sha != ''
id: vm_checksums
run: |
SHA="${{ steps.vm_bundle.outputs.bundle_sha }}"
BASE="https://downloads.claude.ai/vms/linux"
compute_checksum() {
local url="$1" label="$2"
local hash
hash=$(set -o pipefail
curl -fsSL --max-time 600 "$url" \
| zstd -d \
| sha256sum \
| awk '{print $1}'
) || { echo " FAILED: $label"; echo ""; return; }
if [[ ! $hash =~ ^[a-f0-9]{64}$ ]]; then
echo " INVALID hash for $label: $hash"
echo ""
return
fi
echo " $label: $hash"
echo "$hash"
}
echo "Computing x64 checksums..."
X64_ROOTFS=$(compute_checksum "$BASE/x64/$SHA/rootfs.vhdx.zst" "x64/rootfs.vhdx")
X64_VMLINUZ=$(compute_checksum "$BASE/x64/$SHA/vmlinuz.zst" "x64/vmlinuz")
X64_INITRD=$(compute_checksum "$BASE/x64/$SHA/initrd.zst" "x64/initrd")
echo "Computing arm64 checksums..."
ARM64_ROOTFS=$(compute_checksum "$BASE/arm64/$SHA/rootfs.vhdx.zst" "arm64/rootfs.vhdx")
ARM64_VMLINUZ=$(compute_checksum "$BASE/arm64/$SHA/vmlinuz.zst" "arm64/vmlinuz")
ARM64_INITRD=$(compute_checksum "$BASE/arm64/$SHA/initrd.zst" "arm64/initrd")
echo "x64_rootfs=$X64_ROOTFS" >> $GITHUB_OUTPUT
echo "x64_vmlinuz=$X64_VMLINUZ" >> $GITHUB_OUTPUT
echo "x64_initrd=$X64_INITRD" >> $GITHUB_OUTPUT
echo "arm64_rootfs=$ARM64_ROOTFS" >> $GITHUB_OUTPUT
echo "arm64_vmlinuz=$ARM64_VMLINUZ" >> $GITHUB_OUTPUT
echo "arm64_initrd=$ARM64_INITRD" >> $GITHUB_OUTPUT
- name: Update build.sh VM checksums
if: steps.check_update.outputs.update_needed == 'true' && steps.vm_bundle.outputs.bundle_sha != ''
run: |
update_vm_checksum() {
local var="$1" value="$2"
if [ -z "$value" ]; then
echo " Skipping $var (empty)"
return
fi
sed -i "s/${var}='[^']*'/${var}='${value}'/" build.sh
}
echo "Updating build.sh VM checksums..."
update_vm_checksum vm_checksum_x64_rootfs_vhdx "${{ steps.vm_checksums.outputs.x64_rootfs }}"
update_vm_checksum vm_checksum_x64_vmlinuz "${{ steps.vm_checksums.outputs.x64_vmlinuz }}"
update_vm_checksum vm_checksum_x64_initrd "${{ steps.vm_checksums.outputs.x64_initrd }}"
update_vm_checksum vm_checksum_arm64_rootfs_vhdx "${{ steps.vm_checksums.outputs.arm64_rootfs }}"
update_vm_checksum vm_checksum_arm64_vmlinuz "${{ steps.vm_checksums.outputs.arm64_vmlinuz }}"
update_vm_checksum vm_checksum_arm64_initrd "${{ steps.vm_checksums.outputs.arm64_initrd }}"
echo "Verifying updates:"
grep "vm_checksum_" build.sh
- name: Update Nix package
if: steps.check_update.outputs.update_needed == 'true'
run: |

View File

@@ -124,6 +124,18 @@ detect_architecture() {
echo "Target Architecture: $architecture"
section_footer 'Architecture Detection'
# VM bundle checksums — SHA-256 of decompressed linux CDN files.
# Updated automatically by check-claude-version.yml when the
# bundle SHA changes. Used in patch_cowork_linux() Patch 4 to
# inject correct linux manifest entries. If empty, Patch 4 falls
# back to copying win32 checksums (which may not match).
vm_checksum_x64_rootfs_vhdx='a829fe446f24d5e49dcca7f4c61042cf79bc53197d06c582f3fac69282131410'
vm_checksum_x64_vmlinuz='9ab0e4031fbdf90c5133a18c0ab399e9abc0a5935777ac5b29c1e26dba8b6596'
vm_checksum_x64_initrd='032214290388d688790d7b169575a5f75693543b21b33869423713411c12bd6d'
vm_checksum_arm64_rootfs_vhdx='d6eb347a6914839514c42b262baeba48df81517cd7fdfbeb0e435dd98cbb7105'
vm_checksum_arm64_vmlinuz='ce876786f908390c65a8a58a442399a864875e13eae8da5e4ebc8c0255772515'
vm_checksum_arm64_initrd='c75a1aff9719bf09527a8f9f055b3fb6e2c0633973efc8295cf07e46bb6333e1'
}
detect_distro() {
@@ -960,7 +972,14 @@ patch_cowork_linux() {
# All complex patches are done via node to avoid shell escaping issues
# with minified JavaScript. Uses unique string anchors and dynamic
# variable extraction to be version-agnostic per CLAUDE.md guidelines.
if ! INDEX_JS="$index_js" SVC_PATH="cowork-vm-service.js" node << 'COWORK_PATCH'
if ! INDEX_JS="$index_js" SVC_PATH="cowork-vm-service.js" \
VM_CS_X64_ROOTFS="$vm_checksum_x64_rootfs_vhdx" \
VM_CS_X64_VMLINUZ="$vm_checksum_x64_vmlinuz" \
VM_CS_X64_INITRD="$vm_checksum_x64_initrd" \
VM_CS_ARM64_ROOTFS="$vm_checksum_arm64_rootfs_vhdx" \
VM_CS_ARM64_VMLINUZ="$vm_checksum_arm64_vmlinuz" \
VM_CS_ARM64_INITRD="$vm_checksum_arm64_initrd" \
node << 'COWORK_PATCH'
const fs = require('fs');
const indexJs = process.env.INDEX_JS;
let code = fs.readFileSync(indexJs, 'utf8');
@@ -1091,35 +1110,70 @@ if (pipeMatch) {
// ============================================================
// Patch 4: Bundle manifest - add Linux entries to Ln.files
// Anchor: find files:{darwin: near rootfs.img checksum pattern
// Extracts the win32 file entries (rootfs.vhdx, vmlinuz, initrd
// with checksums) and reuses them as linux entries so the app's
// built-in download infrastructure fetches VM images for Linux.
// Falls back to empty arrays if win32 extraction fails.
// Extracts the win32 file entries (rootfs.vhdx, vmlinuz, initrd)
// for structure (names, progressStart/End), then replaces the
// checksums with correct linux values from build-time env vars.
// Falls back to win32 checksums if linux checksums unavailable.
// ============================================================
const linuxChecksums = {
x64: {
'rootfs.vhdx': process.env.VM_CS_X64_ROOTFS || '',
'vmlinuz': process.env.VM_CS_X64_VMLINUZ || '',
'initrd': process.env.VM_CS_X64_INITRD || '',
},
arm64: {
'rootfs.vhdx': process.env.VM_CS_ARM64_ROOTFS || '',
'vmlinuz': process.env.VM_CS_ARM64_VMLINUZ || '',
'initrd': process.env.VM_CS_ARM64_INITRD || '',
},
};
function replaceChecksums(archArray, arch) {
const checksums = linuxChecksums[arch];
const hasChecksums = Object.values(checksums).every(
v => /^[a-f0-9]{64}$/.test(v));
if (!hasChecksums) {
console.log(` WARNING: Missing linux ${arch} checksums,` +
' using win32 values as fallback');
return archArray;
}
let result = archArray;
for (const [name, hash] of Object.entries(checksums)) {
// Match: checksum:"<64-hex>" near name:"<name>"
// The checksum field follows the name field in each entry
const nameIdx = result.indexOf('"' + name + '"');
if (nameIdx === -1) continue;
const afterName = result.substring(nameIdx);
const csRe = /checksum\s*:\s*"([a-f0-9]{64})"/;
const csMatch = afterName.match(csRe);
if (csMatch) {
result = result.substring(0, nameIdx) +
afterName.replace(csMatch[0],
'checksum:"' + hash + '"');
}
}
console.log(` Replaced ${arch} checksums with linux values`);
return result;
}
if (!code.includes('"linux":{') && !code.includes("'linux':{") &&
!code.includes('linux:{')) {
// Find the manifest SHA (40-char hex near files:{)
const shaRe = /sha\s*:\s*"([a-f0-9]{40})"/;
const shaMatch = code.match(shaRe);
if (shaMatch) {
// Find 'files:' or 'files :' after the sha
const shaIdx = code.indexOf(shaMatch[0]);
const afterSha = code.indexOf('files', shaIdx);
if (afterSha !== -1 && afterSha - shaIdx < 200) {
// Find the opening brace of files object
// Extract the full files:{...} block
const filesBlock = extractBlock(code, afterSha, '{');
if (filesBlock) {
const filesEnd = code.indexOf(filesBlock, afterSha)
+ filesBlock.length;
// Extract win32 x64 and arm64 arrays
// Extract win32 x64 and arm64 arrays for structure
let win32x64 = null;
let win32arm64 = null;
const win32Idx = filesBlock.indexOf('win32');
if (win32Idx !== -1) {
// Scope to win32:{...} to avoid matching
// x64/arm64 from darwin or other platforms
const win32Block =
extractBlock(filesBlock, win32Idx, '{');
if (win32Block) {
@@ -1136,33 +1190,36 @@ if (!code.includes('"linux":{') && !code.includes("'linux':{") &&
}
}
// Build linux entry: use extracted win32 arrays, or
// fall back to empty arrays (vacuous truth)
// Build linux entries with correct checksums
let linuxX64 = '[]';
let linuxArm64 = '[]';
if (win32x64 && win32x64.includes('name')) {
linuxX64 = win32x64;
console.log(' Extracted win32 x64 file entries for linux');
linuxX64 = replaceChecksums(win32x64, 'x64');
console.log(' Built linux x64 entries from' +
' win32 structure');
}
if (win32arm64 && win32arm64.includes('name')) {
linuxArm64 = win32arm64;
console.log(' Extracted win32 arm64 file entries for linux');
linuxArm64 = replaceChecksums(
win32arm64, 'arm64');
console.log(' Built linux arm64 entries from' +
' win32 structure');
}
// Insert linux entry before the closing } of files
const insertPos = filesEnd - 1;
const linuxEntry =
',linux:{x64:' + linuxX64 +
',arm64:' + linuxArm64 + '}';
code = code.substring(0, insertPos) +
linuxEntry + code.substring(insertPos);
console.log(' Added Linux entries to bundle manifest');
console.log(' Added Linux entries to bundle' +
' manifest');
patchCount++;
}
}
}
if (!code.includes('linux:{x64:')) {
console.log(' WARNING: Could not add Linux bundle manifest entries');
console.log(' WARNING: Could not add Linux bundle' +
' manifest entries');
}
}