Reject duplicate source generation references
This commit is contained in:
@@ -82,7 +82,7 @@ snapshot은 새 회차 폴더를 만들 때 한 번 저장하고 공유 입력
|
|||||||
|
|
||||||
## 원본 manifest 무결성 검증 보완
|
## 원본 manifest 무결성 검증 보완
|
||||||
|
|
||||||
재사용 환경의 `sourceGenerations[].manifestSha256`를 단순히 기록하는 데 그치지 않고, 가리키는 원본 manifest 파일의 실제 SHA-256과 대조합니다. 참조 경로와 runId가 일치하는지, 복사한 생성 환경이 원본 환경과 같은지도 확인합니다. 따라서 WAV·대본·프로필은 그대로 둔 채 원본의 장치 이름 등 환경 기록만 변조해도 검증이 실패합니다.
|
재사용 환경의 `sourceGenerations[].manifestSha256`를 단순히 기록하는 데 그치지 않고, 가리키는 원본 manifest 파일의 실제 SHA-256과 대조합니다. 참조 경로와 runId가 일치하는지, 중복 runId가 없는지, 복사한 생성 환경이 원본 환경과 같은지도 확인합니다. 따라서 WAV·대본·프로필은 그대로 둔 채 원본의 장치 이름 등 환경 기록만 변조해도 검증이 실패합니다.
|
||||||
|
|
||||||
sidecar 전환 당시 `input-snapshot-migration.json`의 네 `runs[].originalManifestSha256`는 변경하지 않은 원본 manifest 전체 파일을 가리킵니다. 현재 실제 네 파일과 기록된 SHA가 모두 일치함을 재확인했으며, 해당 legacy 회차를 읽을 때 이 해시도 검증합니다. 전환 때 점검한 `preservedArtifactSha256`의 공유 카탈로그·프로필 같은 항목은 그 시점의 보존 증거이므로 앞으로의 정상적인 입력 변경을 금지하는 고정값으로 사용하지 않습니다. 현재 입력 대조는 별도의 기존 catalog/profile 검사로 수행합니다.
|
sidecar 전환 당시 `input-snapshot-migration.json`의 네 `runs[].originalManifestSha256`는 변경하지 않은 원본 manifest 전체 파일을 가리킵니다. 현재 실제 네 파일과 기록된 SHA가 모두 일치함을 재확인했으며, 해당 legacy 회차를 읽을 때 이 해시도 검증합니다. 전환 때 점검한 `preservedArtifactSha256`의 공유 카탈로그·프로필 같은 항목은 그 시점의 보존 증거이므로 앞으로의 정상적인 입력 변경을 금지하는 고정값으로 사용하지 않습니다. 현재 입력 대조는 별도의 기존 catalog/profile 검사로 수행합니다.
|
||||||
|
|
||||||
|
|||||||
@@ -114,10 +114,14 @@ def load_run_inputs(manifest, root):
|
|||||||
migrated = [record for record in load_json(migration_path)['runs'] if record['runId'] == run_id]
|
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']):
|
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')
|
raise ValueError(f'{run_id}: immutable legacy manifest SHA-256 mismatch')
|
||||||
|
source_ids = set()
|
||||||
for reference in manifest.get('environment', {}).get('sourceGenerations', []):
|
for reference in manifest.get('environment', {}).get('sourceGenerations', []):
|
||||||
source_id = reference['runId']
|
source_id = reference['runId']
|
||||||
if not isinstance(source_id, str) or not re.fullmatch(r'[A-Za-z0-9_]+', source_id):
|
if not isinstance(source_id, str) or not re.fullmatch(r'[A-Za-z0-9_]+', source_id):
|
||||||
raise ValueError('invalid source generation reference runId')
|
raise ValueError('invalid source generation reference runId')
|
||||||
|
if source_id in source_ids:
|
||||||
|
raise ValueError('duplicate source generation reference runId')
|
||||||
|
source_ids.add(source_id)
|
||||||
if reference['manifest'] != f'generated/{source_id}/manifest.json':
|
if reference['manifest'] != f'generated/{source_id}/manifest.json':
|
||||||
raise ValueError('source generation reference must identify its own run manifest')
|
raise ValueError('source generation reference must identify its own run manifest')
|
||||||
source_path = Path(root) / reference['manifest']
|
source_path = Path(root) / reference['manifest']
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ class VoiceHistoryTests(unittest.TestCase):
|
|||||||
changed_environment = copy.deepcopy(second)
|
changed_environment = copy.deepcopy(second)
|
||||||
changed_environment['environment']['sourceGenerations'][0]['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
changed_environment['environment']['sourceGenerations'][0]['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
||||||
self.assertTrue(voice.validate_manifest(changed_environment, self.catalog, self.profile, self.root))
|
self.assertTrue(voice.validate_manifest(changed_environment, self.catalog, self.profile, self.root))
|
||||||
|
duplicated = copy.deepcopy(second)
|
||||||
|
duplicated['environment']['sourceGenerations'].append(copy.deepcopy(duplicated['environment']['sourceGenerations'][0]))
|
||||||
|
self.assertTrue(voice.validate_manifest(duplicated, self.catalog, self.profile, self.root))
|
||||||
|
changed_path = copy.deepcopy(second)
|
||||||
|
changed_path['environment']['sourceGenerations'][0]['manifest'] = '../outside/manifest.json'
|
||||||
|
self.assertTrue(voice.validate_manifest(changed_path, self.catalog, self.profile, self.root))
|
||||||
origin = self.root / 'generated' / first['runId'] / 'manifest.json'
|
origin = self.root / 'generated' / first['runId'] / 'manifest.json'
|
||||||
changed = voice.load_json(origin)
|
changed = voice.load_json(origin)
|
||||||
changed['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
changed['environment']['device'] = 'CORRUPTED_DEVICE_RECORD'
|
||||||
|
|||||||
Reference in New Issue
Block a user