Verify recorded source manifest SHA and immutable legacy history
This commit is contained in:
@@ -109,6 +109,23 @@ def load_run_inputs(manifest, root):
|
||||
validate_inputs(catalog, profile)
|
||||
if profile['modelRevision'] != manifest.get('modelRevision'):
|
||||
raise ValueError('input snapshot model revision differs from manifest')
|
||||
migration_path = Path(root) / 'input-snapshot-migration.json'
|
||||
if migration_path.exists():
|
||||
migrated = [record for record in load_json(migration_path)['runs'] if record['runId'] == run_id]
|
||||
if migrated and (len(migrated) != 1 or file_hash(run_dir / 'manifest.json') != migrated[0]['originalManifestSha256']):
|
||||
raise ValueError(f'{run_id}: immutable legacy manifest SHA-256 mismatch')
|
||||
for reference in manifest.get('environment', {}).get('sourceGenerations', []):
|
||||
source_id = reference['runId']
|
||||
if not isinstance(source_id, str) or not re.fullmatch(r'[A-Za-z0-9_]+', source_id):
|
||||
raise ValueError('invalid source generation reference runId')
|
||||
if reference['manifest'] != f'generated/{source_id}/manifest.json':
|
||||
raise ValueError('source generation reference must identify its own run manifest')
|
||||
source_path = Path(root) / reference['manifest']
|
||||
if file_hash(source_path) != reference['manifestSha256']:
|
||||
raise ValueError(f'{source_id}: recorded source manifest SHA-256 mismatch')
|
||||
source = load_json(source_path)
|
||||
if source.get('runId') != source_id or source.get('environment') != reference.get('environment'):
|
||||
raise ValueError(f'{source_id}: source generation environment reference differs')
|
||||
clips = {clip['id']: clip for clip in catalog['clips']}
|
||||
for entry in manifest.get('clips', []):
|
||||
clip = clips.get(entry.get('id'))
|
||||
|
||||
@@ -223,6 +223,39 @@ class VoiceHistoryTests(unittest.TestCase):
|
||||
self.assertEqual([], voice.validate_manifest(legacy, self.catalog, self.profile, self.root))
|
||||
self.assertEqual(saved_manifest, (self.root / 'generated' / manifest['runId'] / 'manifest.json').read_bytes())
|
||||
|
||||
def test_reuse_rejects_origin_manifest_device_changed_after_sha_recording(self):
|
||||
first = self.run_generation()
|
||||
entries = voice.reusable_entries(self.root, self.catalog, self.profile)
|
||||
with mock.patch.object(voice.importlib.metadata, 'version', return_value='test-version'), \
|
||||
mock.patch.object(voice.subprocess, 'run', return_value=mock.Mock(stdout='Test GPU\n')):
|
||||
environment = voice.reuse_environment(self.profile, entries, self.root)
|
||||
second = voice.generate(self.catalog, self.profile, self.root, environment=environment)
|
||||
self.assertEqual([], voice.validate_manifest(second, self.catalog, self.profile, self.root))
|
||||
changed_reference = copy.deepcopy(second)
|
||||
changed_reference['environment']['sourceGenerations'][0]['manifestSha256'] = '0' * 64
|
||||
self.assertTrue(voice.validate_manifest(changed_reference, self.catalog, self.profile, self.root))
|
||||
changed_environment = copy.deepcopy(second)
|
||||
changed_environment['environment']['sourceGenerations'][0]['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
||||
self.assertTrue(voice.validate_manifest(changed_environment, self.catalog, self.profile, self.root))
|
||||
origin = self.root / 'generated' / first['runId'] / 'manifest.json'
|
||||
changed = voice.load_json(origin)
|
||||
changed['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
||||
voice.write_json(origin, changed)
|
||||
self.assertTrue(voice.validate_manifest(second, self.catalog, self.profile, self.root))
|
||||
|
||||
def test_legacy_migration_sha_seals_manifest_without_freezing_mutable_inputs(self):
|
||||
manifest = self.run_generation()
|
||||
path = self.root / 'generated' / manifest['runId'] / 'manifest.json'
|
||||
voice.write_json(self.root / 'input-snapshot-migration.json', {
|
||||
'schemaVersion': 1, 'runs': [{'runId': manifest['runId'], 'originalManifestSha256': voice.file_hash(path)}],
|
||||
'preservedArtifactSha256': {'qwen-profile.json': 'old mutable file hash'}})
|
||||
voice.write_json(self.root / 'qwen-profile.json', {'seed': 43})
|
||||
self.assertEqual([], voice.validate_manifest(manifest, self.catalog, self.profile, self.root))
|
||||
changed = voice.load_json(path)
|
||||
changed['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
||||
voice.write_json(path, changed)
|
||||
self.assertTrue(voice.validate_manifest(changed, self.catalog, self.profile, self.root))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user