Enforce hash-pinned confirmation training gate

This commit is contained in:
rUv committed 2026-10-13 01:01:11 -07:00
1 parent 8c77729f73
commit 464cc3b59e
5 files changed
+251 -2

No files matched your search

+49
View File
@@ -7,6 +7,7 @@ import re
import stat
MAX_BYTES = 2 * 1024 * 1024
MAX_DATASET_MANIFEST_BYTES = 3_000_000
MAX_RECORDS = 4096
SPLITS = ('train', 'development', 'confirmation')
AXES = ('room', 'participant', 'session')
@@ -126,6 +127,54 @@ def read_json(path):
return json.loads(raw, object_pairs_hook=_pairs, parse_constant=invalid)
def regular_sha256(path, maximum=MAX_DATASET_MANIFEST_BYTES):
"""Hash one bounded regular file through the descriptor that was checked."""
if (type(maximum) is not int or not 1 <= maximum <= 16 * 1024 * 1024
or not hasattr(os, 'O_NOFOLLOW') or not hasattr(os, 'O_NONBLOCK')):
raise ValueError('Safe file hashing unavailable')
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
digest_value = hashlib.sha256()
with os.fdopen(fd, 'rb') as handle:
info = os.fstat(handle.fileno())
if not stat.S_ISREG(info.st_mode) or info.st_size > maximum:
raise ValueError('Expected bounded regular file')
remaining = info.st_size
while remaining:
block = handle.read(min(1024 * 1024, remaining))
if not block:
raise ValueError('File changed during hashing')
digest_value.update(block)
remaining -= len(block)
if handle.read(1):
raise ValueError('File changed during hashing')
return digest_value.hexdigest()
def training_gate(manifest_path, ledger_path, dataset_manifest_path,
expected_protocol_sha256, expected_dataset_manifest_sha256):
"""Evaluate declared metadata and bind it to frozen protocol/dataset bytes.
This remains a metadata gate. It does not authenticate captures, consent,
licensing, source aliases, or the completeness of the exposure ledger.
"""
digest(expected_protocol_sha256)
digest(expected_dataset_manifest_sha256)
report = evaluate(read_json(manifest_path), read_json(ledger_path))
reasons = set(report['reasons'])
if report['protocol_sha256'] != expected_protocol_sha256:
reasons.add('protocol_digest_mismatch')
actual = regular_sha256(dataset_manifest_path)
if actual != expected_dataset_manifest_sha256:
reasons.add('dataset_manifest_digest_mismatch')
result = dict(report)
result.update({'preflight_passed': not reasons,
'reasons': sorted(reasons),
'dataset_manifest_sha256': actual,
'expected_dataset_manifest_sha256': expected_dataset_manifest_sha256,
'scope': report['scope'] + ' Dataset bytes are hash-pinned; provenance claims remain unauthenticated.'})
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--manifest', required=True)
+28
View File
@@ -363,9 +363,37 @@ def main(argv=None):
parser.add_argument('--seed', type=int, default=42)
parser.add_argument('--max-seconds', type=int, default=600)
parser.add_argument('--optimize-bundle', help='Existing statistics bundle; inference only, no training or test evaluation')
parser.add_argument('--confirmation-manifest', help='Independent-confirmation metadata plan (enables enforced gate)')
parser.add_argument('--exposure-ledger', help='Previously exposed dataset revisions for the confirmation gate')
parser.add_argument('--expected-protocol-sha256', help='Frozen protocol digest for the confirmation gate')
parser.add_argument('--expected-dataset-manifest-sha256', help='Frozen digest of --manifest bytes')
args = parser.parse_args(argv)
gate_names = ('confirmation_manifest', 'exposure_ledger', 'expected_protocol_sha256',
'expected_dataset_manifest_sha256')
gate_values = [getattr(args, name) for name in gate_names]
gate_enabled = all(gate_values)
if (any(gate_values) and not gate_enabled) or (gate_enabled and args.optimize_bundle):
print(json.dumps({'status': 'invalid_input', 'promotionAuthorized': False}))
raise SystemExit(1)
preflight = None
if gate_enabled:
from .confirmation import training_gate
try:
preflight = training_gate(args.confirmation_manifest, args.exposure_ledger,
args.manifest, args.expected_protocol_sha256,
args.expected_dataset_manifest_sha256)
except (ValueError, OSError, TypeError, RecursionError, OverflowError):
print(json.dumps({'status': 'invalid_input', 'promotionAuthorized': False}))
raise SystemExit(1)
if not preflight['preflight_passed']:
print(json.dumps(preflight, sort_keys=True))
raise SystemExit(2)
from .espfi import load_dataset
partitions, provenance = load_dataset(args.manifest)
if preflight:
if provenance.get('manifest_sha256') != preflight['dataset_manifest_sha256']:
raise ValueError('Dataset manifest changed after confirmation preflight')
provenance = dict(provenance, confirmation_preflight=preflight)
if args.optimize_bundle:
parent = json.loads((Path(args.optimize_bundle) / 'metrics.json').read_text())
if provenance['manifest_sha256'] != parent['provenance']['manifest_sha256']:
@@ -7,7 +7,7 @@ import subprocess
import sys
import tempfile
import unittest
from csi_training.confirmation import evaluate, read_json, KNOWN_EXPOSED
from csi_training.confirmation import evaluate, read_json, regular_sha256, training_gate, KNOWN_EXPOSED
def fixture():
@@ -92,4 +92,28 @@ class ConfirmationTests(unittest.TestCase):
self.assertEqual(r.returncode,expected,r.stderr)
self.assertFalse(json.loads(r.stdout)['promotionAuthorized'])
def test_training_gate_binds_frozen_protocol_and_dataset(self):
with tempfile.TemporaryDirectory() as d:
m,l=fixture();root=Path(d);dataset=root/'dataset.json'
p=root/'m.json';q=root/'l.json'
dataset.write_text('{"schema_version":1}')
p.write_text(json.dumps(m));q.write_text(json.dumps(l))
frozen=regular_sha256(dataset)
report=training_gate(p,q,dataset,'a'*64,frozen)
self.assertTrue(report['preflight_passed'])
self.assertFalse(report['promotionAuthorized'])
self.assertNotIn('records',report)
self.assertIn('protocol_digest_mismatch',training_gate(p,q,dataset,'b'*64,frozen)['reasons'])
self.assertIn('dataset_manifest_digest_mismatch',training_gate(p,q,dataset,'a'*64,'0'*64)['reasons'])
def test_regular_sha256_rejects_unsafe_files(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);target=root/'target';target.write_bytes(b'ok')
link=root/'link';link.symlink_to(target)
with self.assertRaises(OSError):regular_sha256(link)
fifo=root/'fifo';os.mkfifo(fifo)
with self.assertRaises(ValueError):regular_sha256(fifo)
oversized=root/'oversized';oversized.write_bytes(b'x'*33)
with self.assertRaises(ValueError):regular_sha256(oversized,32)
if __name__=='__main__':unittest.main()
+110 -1
View File
@@ -1,15 +1,19 @@
"""Synthetic integrity tests, not RF sensing accuracy evidence."""
import copy
import json
import os
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
import numpy as np
import torch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from csi_training.espfi_train import AmplitudeBundle, AmplitudeClassifier, optimize, optimize_bundle, scores
from csi_training.espfi_train import AmplitudeBundle, AmplitudeClassifier, main, optimize, optimize_bundle, scores
from csi_training.confirmation import regular_sha256
from scripts.tests.test_csi_training_confirmation import fixture as confirmation_fixture
def fixture():
@@ -107,5 +111,110 @@ class TrainerTests(unittest.TestCase):
AmplitudeBundle(path)
class ConfirmationGateIntegrationTests(unittest.TestCase):
def gate_files(self, root):
plan, ledger = confirmation_fixture()
dataset = root / 'dataset.json'
plan_path, ledger_path = root / 'plan.json', root / 'ledger.json'
dataset.write_text('{"schema_version":1}')
plan_path.write_text(json.dumps(plan))
ledger_path.write_text(json.dumps(ledger))
return dataset, plan_path, ledger_path
def argv(self, root, dataset, plan, ledger):
return ['--manifest', str(dataset), '--output', str(root / 'out'),
'--confirmation-manifest', str(plan), '--exposure-ledger', str(ledger),
'--expected-protocol-sha256', 'a' * 64,
'--expected-dataset-manifest-sha256', regular_sha256(dataset)]
def test_blocked_gate_never_loads_or_trains(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
value=json.loads(plan.read_text());value['records'][-1]['room']=value['records'][0]['room']
plan.write_text(json.dumps(value))
with patch('csi_training.espfi.load_dataset') as load, patch('csi_training.espfi_train.optimize') as train:
with self.assertRaisesRegex(SystemExit, '2'):
main(self.argv(root,dataset,plan,ledger))
load.assert_not_called();train.assert_not_called()
self.assertFalse((root/'out').exists())
def test_invalid_partial_and_hash_mismatch_never_load(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
cases=[self.argv(root,dataset,plan,ledger)[:-2],
self.argv(root,dataset,plan,ledger)[:-1]+['0'*64]]
plan.write_text('{bad json')
cases.append(self.argv(root,dataset,plan,ledger))
for args in cases:
with self.subTest(args=args[-2:]), patch('csi_training.espfi.load_dataset') as load, patch('csi_training.espfi_train.optimize') as train:
with self.assertRaises(SystemExit):main(args)
load.assert_not_called();train.assert_not_called()
self.assertFalse((root/'out').exists())
def test_unsafe_gate_files_never_load(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
valid_plan=plan.read_bytes()
unsafe=[]
plan.unlink();plan.symlink_to(ledger);unsafe.append(plan)
for candidate in unsafe:
with patch('csi_training.espfi.load_dataset') as load:
with self.assertRaisesRegex(SystemExit,'1'):
main(self.argv(root,dataset,candidate,ledger))
load.assert_not_called()
plan.unlink();os.mkfifo(plan)
with patch('csi_training.espfi.load_dataset') as load:
with self.assertRaisesRegex(SystemExit,'1'):
main(self.argv(root,dataset,plan,ledger))
load.assert_not_called()
plan.unlink();plan.write_bytes(valid_plan+b' '*(2*1024*1024))
with patch('csi_training.espfi.load_dataset') as load:
with self.assertRaisesRegex(SystemExit,'1'):
main(self.argv(root,dataset,plan,ledger))
load.assert_not_called()
def test_protocol_mismatch_never_loads(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
args=self.argv(root,dataset,plan,ledger)
args[args.index('--expected-protocol-sha256')+1]='b'*64
with patch('csi_training.espfi.load_dataset') as load, patch('csi_training.espfi_train.optimize') as train:
with self.assertRaisesRegex(SystemExit,'2'):main(args)
load.assert_not_called();train.assert_not_called()
def test_manifest_race_stops_before_training(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
parts,provenance=fixture();provenance['manifest_sha256']='f'*64
with patch('csi_training.espfi.load_dataset',return_value=(parts,provenance)) as load, \
patch('csi_training.espfi_train.optimize') as train:
with self.assertRaisesRegex(ValueError,'changed'):
main(self.argv(root,dataset,plan,ledger))
load.assert_called_once();train.assert_not_called()
self.assertFalse((root/'out').exists())
def test_gate_rejects_bundle_optimization_before_loading(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
args=self.argv(root,dataset,plan,ledger)+['--optimize-bundle',str(root/'bundle')]
with patch('csi_training.espfi.load_dataset') as load, patch('csi_training.espfi_train.optimize_bundle') as optimize_existing:
with self.assertRaisesRegex(SystemExit,'1'):main(args)
load.assert_not_called();optimize_existing.assert_not_called()
def test_passing_gate_attaches_sanitized_provenance(self):
with tempfile.TemporaryDirectory() as d:
root=Path(d);dataset,plan,ledger=self.gate_files(root)
parts,provenance=fixture();provenance['manifest_sha256']=regular_sha256(dataset)
report={'variant':'statistics','test':{},'latency':{},'promotionAuthorized':False}
with patch('csi_training.espfi.load_dataset',return_value=(parts,provenance)) as load, \
patch('csi_training.espfi_train.optimize',return_value=report) as train:
main(self.argv(root,dataset,plan,ledger))
load.assert_called_once();train.assert_called_once()
attached=train.call_args.args[1]['confirmation_preflight']
self.assertTrue(attached['preflight_passed'])
self.assertFalse(attached['promotionAuthorized'])
self.assertNotIn('records',attached)
if __name__ == '__main__':
unittest.main()