VulpesOS/tundra/tools/sargo_workspace.py
OverSu d04824ec89 Prepare Sargo installations with private per-device workspaces
Bind backups, storage layout, RAM builds, file staging and recovery to each phone. Verify atomic transfers without replacing existing state and record the unresolved Android HAL source provenance.

Validated with 53 native tooling tests, 5 changelog tests and two RAM boots with persistent Gaia state on the development Sargo. No new flash or native binary release.
2026-10-04 00:06:13 +02:00

70 lines
3.4 KiB
Python

# SPDX-License-Identifier: MPL-2.0
"""Private, portable inputs for one Sargo. No shared device inventory paths."""
import hashlib
import json
from pathlib import Path
import re
import struct
from boot_backup import verify
from storage_layout import mount_table
from tundra import digest
# The system/HAL has only been exercised with this kernel and boot geometry.
KERNEL_SHA256 = 'e214e5e363bcf4db6387668ba723299e75009bc128fb0088f706cfb311db0080'
def boot_kernel(path):
with Path(path).open('rb') as stream:
header = stream.read(608)
if len(header) != 608 or header[:8] != b'ANDROID!':
raise ValueError('Expected an Android boot image')
fields = struct.unpack_from('<10I', header, 8)
size, address, ramdisk_size, ramdisk_address, second_size, _, tags, page, version, _ = fields
if (version != 0 or page != 4096 or address != 0x8000
or ramdisk_address != 0x1000000 or tags != 0x100 or second_size
or not 0 < size < 64*1024**2 or not 0 < ramdisk_size < 64*1024**2):
raise ValueError('Boot geometry is not supported by this Sargo adaptation')
ramdisk_offset = page + ((size+page-1)//page)*page
if ramdisk_offset+ramdisk_size > Path(path).stat().st_size:
raise ValueError('Boot payload exceeds the backup')
stream.seek(page)
kernel = stream.read(size)
if len(kernel) != size:
raise ValueError('Truncated kernel')
result = hashlib.sha256(kernel).hexdigest()
if result != KERNEL_SHA256:
raise ValueError('Kernel has not been qualified with this system/HAL')
return result
def checked_workspace(folder):
folder = Path(folder).resolve()
if (folder/'.incomplete').exists():
raise ValueError('Device workspace preparation did not finish')
value = json.loads((folder/'device.json').read_text())
if (value.get('schema') != 1 or value.get('device') != 'sargo'
or value.get('activeSlot') != 'a'
or not re.fullmatch(r'[a-f0-9]{64}', str(value.get('serialSha256', '')))):
raise ValueError('Expected a Sargo slot A workspace with a device identity')
for name, sha in value.get('files', {}).items():
path = folder/name
if (name not in ('backup/manifest.json', 'layout.json') or path.is_symlink()
or not path.resolve().is_relative_to(folder) or digest(path) != sha):
raise ValueError('Workspace input changed: '+name)
if set(value.get('files', {})) != {'backup/manifest.json', 'layout.json'}:
raise ValueError('Workspace is missing its backup or storage inventory')
backup = verify(folder/'backup')
layout = json.loads((folder/'layout.json').read_text())
for record in (backup, layout):
if record.get('serialSha256') != value['serialSha256'] or record.get('activeSlot') != 'a':
raise ValueError('Storage and boot backup must belong to the same phone on slot A')
mount_table(layout)
kernel = boot_kernel(folder/'backup/boot_a.img')
if kernel != value.get('kernelSha256'):
raise ValueError('Workspace kernel checksum changed')
plan = {'device': 'sargo', 'activeSlot': 'a', 'serialSha256': value['serialSha256'],
'backupDirectory': str(folder/'backup'),
'backupManifestSha256': value['files']['backup/manifest.json'],
'workspaceSha256': digest(folder/'device.json')}
return plan, layout