"""Bounded native adapter. No engine modifications; no network calls. The adapter validates the vanilla game network envelope before native dispatch. """ import gzip import hashlib import io import json import math import os from pathlib import Path import subprocess import struct import tempfile import time import uuid from training_config import validate_config from build_info import build_identity from analysis_result import assemble_analysis from star_check import check_ticks, needs_observations, resolve_check MODULE_DIR = Path(__file__).resolve().parent # Source checkout is nested; a hosted release is flat under /app. Deployment # supplies explicit binary/template paths, so importing must not assume depth. ROOT = MODULE_DIR.parent.parent if MODULE_DIR.name=='hf' and MODULE_DIR.parent.name=='integrations' else MODULE_DIR def read_json(data): if len(data)>1024*1024: raise ValueError('Input exceeds 1 MB') if data[:2]==b'\x1f\x8b': with gzip.GzipFile(fileobj=io.BytesIO(data)) as stream: data=stream.read(4*1024*1024+1) if len(data)>4*1024*1024: raise ValueError('Expanded input exceeds 4 MB') result=json.loads(data,parse_constant=lambda _: (_ for _ in ()).throw(ValueError('Nonfinite JSON'))) if not isinstance(result,dict): raise ValueError('Expected JSON object') return result OUTPUT_NAMES = ('Steering', 'Acceleration', 'Handbrake', 'Brake', 'Boost') VEHICLE_NAMES = ('Formula', 'Rally', 'Snowmobile', 'Truck') SCALAR_SENSORS = {'speed', 'velocityFront', 'velocitySide', 'angularVelocity', 'wheelAngle', 'boostCapacity', 'correctDirection'} def sensor_input(sensor): if not isinstance(sensor, dict): raise ValueError('Sensor object required') kind = sensor.get('$type') if not isinstance(kind, str): raise ValueError('Sensor type required') clean = {'$type': kind} def number(key, default=None, positive=False): value = sensor.get(key, default) if type(value) not in (int, float) or not math.isfinite(value) or abs(value)>1e6: raise ValueError('Invalid sensor parameter '+key) # The simulator converts descriptor values to float32 before use. if positive and (value<=0 or struct.unpack('f', struct.pack('f', value))[0]<=0): raise ValueError('Positive sensor parameter required: '+key) clean[key] = value return value if kind in SCALAR_SENSORS: return clean if kind == 'raycast': if not -360<=number('Degrees')<=360: raise ValueError('Degrees') if not 1<=number('Length')<=10000: raise ValueError('Length') elif kind in ('accelerationFront', 'accelerationSide'): number('MaxAcceleration', 10 if kind=='accelerationFront' else 12, positive=True) elif kind == 'distanceFromWall': number('MaxDistance', 150, positive=True) elif kind == 'trackCurvature': number('MinLookaheadDistance', 256) number('MaxLookaheadDistance', 512) elif kind == 'grip': offset = sensor.get('PositionOffset', {}) if isinstance(offset, dict): coords = [offset.get('X',0), offset.get('Y',0)] elif isinstance(offset, list) and len(offset)==2: coords = offset else: raise ValueError('Grip offset must have two components') if any(type(n) not in (int,float) or not math.isfinite(n) or abs(n)>1e6 for n in coords): raise ValueError('Invalid grip offset') clean['PositionOffset'] = {'X':coords[0], 'Y':coords[1]} else: raise ValueError('Unsupported sensor type') return clean def network_input(data): value=read_json(data) network=value['NeuralNetwork']; shape=network['Shape']; outputs=value.get('Outputs') vehicle_type=value.get('Type') if type(vehicle_type) is not int or not 0<=vehicle_type20 or any(n>16 for n in shape[1:-1]) or shape[-1]!=len(outputs)): raise ValueError('Shape: up to 20 inputs, 10 hidden layers of 16, and 1–5 outputs') sensors=value['Sensors'] if not isinstance(sensors,list) or len(sensors)!=shape[0]: raise ValueError('Sensor count') clean_sensors=[sensor_input(sensor) for sensor in sensors] rays=sum(s['$type']=='raycast' for s in clean_sensors) if rays>13 or len(sensors)-rays>7: raise ValueError('Use up to 13 vision and 7 other sensors') weights,biases=network['Weights'],network['Biases'] if not isinstance(weights,list) or not isinstance(biases,list) or len(weights)!=len(shape)-1 or len(biases)!=len(shape)-1: raise ValueError('Layer count') for i,(a,b) in enumerate(zip(shape,shape[1:])): if (not isinstance(weights[i],list) or len(weights[i])!=a or any(not isinstance(row,list) or len(row)!=b for row in weights[i]) or not isinstance(biases[i],list) or len(biases[i])!=b): raise ValueError('Dimensions') for n in [x for row in weights[i] for x in row]+biases[i]: if type(n) not in (int,float) or not math.isfinite(n) or abs(n)>1e6: raise ValueError('Weight value') return {'Id':str(uuid.uuid4()),'Stats':{'Generations':[]},'Snapshots':[], 'Type':vehicle_type,'Outputs':outputs[:],'Sensors':clean_sensors, 'Name':'HF candidate','NeuralNetwork':{'Shape':shape,'InputShape':shape[0],'OutputShape':shape[-1], 'NeuronsCount':sum(shape),'TotalParams':sum((a+1)*b for a,b in zip(shape,shape[1:])), 'Weights':weights,'Biases':biases}} def check_freeze_shape(freeze,shape): """Resolve selection against this policy, with at least one trainable parameter.""" if any(layer>=len(shape)-1 for layer in freeze['layers']): raise ValueError('Frozen hidden layer exceeds network shape') if any(index>=shape[0] for index in freeze['inputs']): raise ValueError('Frozen input exceeds sensor count') if any(index>=shape[-1] for index in freeze['outputs']): raise ValueError('Frozen output exceeds output count') layers=set(freeze['layers']);inputs=set(freeze['inputs']);outputs=set(freeze['outputs']) trainable=0 last=len(shape)-2 for layer,(source_count,dest_count) in enumerate(zip(shape,shape[1:])): for source in range(source_count): for dest in range(dest_count): if not (layer+1 in layers or layer==0 and source in inputs or layer==last and dest in outputs): trainable+=1 for dest in range(dest_count): if not (layer+1 in layers or layer==last and dest in outputs): trainable+=1 if not trainable: raise ValueError('Freeze selection leaves no trainable parameters; use frozen evaluation') def verify_frozen_parameters(freeze,starting,trained): """Never publish a result whose native candidate violates the requested mask.""" layers=set(freeze['layers']);inputs=set(freeze['inputs']);outputs=set(freeze['outputs']) last=len(starting['Shape'])-2 for layer,matrix in enumerate(starting['Weights']): for source,row in enumerate(matrix): for dest,value in enumerate(row): if (layer+1 in layers or layer==0 and source in inputs or layer==last and dest in outputs): if value!=trained['weights'][layer][source][dest]: raise ValueError('Native trainer changed a frozen weight') for dest,value in enumerate(starting['Biases'][layer]): if layer+1 in layers or layer==last and dest in outputs: if value!=trained['biases'][layer][dest]: raise ValueError('Native trainer changed a frozen bias') def track_input(data): value=read_json(data) challenges=value.get('Challenges',[]) if not isinstance(challenges,list) or len(challenges)>64: raise ValueError('Track supports at most 64 challenge definitions') for challenge in challenges: if (not isinstance(challenge,dict) or not isinstance(challenge.get('$type'),str) or not 1<=len(challenge['$type'])<=96): raise ValueError('Invalid track challenge type') for key,limit in (('Text',2048),('Id',128)): if key in challenge and (not isinstance(challenge[key],str) or len(challenge[key])>limit): raise ValueError('Invalid track challenge label') tiles=value['Tiles'] if not isinstance(tiles,dict) or not 4<=len(tiles)<=256: raise ValueError('Track tile limit') start=value.get('StartTileConnection') if not isinstance(start,dict): raise ValueError('StartTileConnection must be an object') # Match the local runner's saved-track import: game files can omit default Type. # Normalize only this parsed copy; never rewrite the uploaded source bytes. if 'Type' not in start: start['Type']=0 elif type(start['Type']) is not int: raise ValueError('StartTileConnection.Type must be an integer') return value def native_environment(gpu_library=None): env={'PATH':'/usr/bin:/bin','LANG':'C','RUST_BACKTRACE':'0'} if gpu_library is not None: library=Path(gpu_library).resolve() if not library.is_file(): raise RuntimeError('CUDA library unavailable') # Device/loader plumbing from an active lease, never provider credentials. for key in ('LD_LIBRARY_PATH','LD_PRELOAD','CUDA_VISIBLE_DEVICES','NVIDIA_VISIBLE_DEVICES'): if key in os.environ: env[key]=os.environ[key] env['ALTD_GPU_LIB']=str(library) return env def best_run_lap(history): # Native best_lap_s is generation-local. Include the independently logged # all-time value and prior generations, never a non-finite sentinel. values = [row.get(key) for row in history for key in ('best_lap_s', 'all_time_best_lap_s')] laps = [v for v in values if type(v) in (int, float) and math.isfinite(v) and v > 0] return min(laps) if laps else None def vehicle_scene_template(scene_template, vehicle_type): if type(vehicle_type) is not int or not 0<=vehicle_type3001 or meta.get('requested_ticks')!=check_ticks(track,config) or type(meta.get('lap_score')) not in (float,int) or not math.isfinite(meta['lap_score']) or meta['lap_score']<=0): raise ValueError('Invalid challenge observations') check_track=dict(track,LapScore=meta['lap_score']) return resolve_check(check_track,template,config,observations,**kwargs) except subprocess.TimeoutExpired: return resolve_check(track,template,config,[],error='The star check reached its time limit; trained weights are preserved.',**kwargs) except (OSError,subprocess.CalledProcessError,ValueError,KeyError,TypeError): return resolve_check(track,template,config,[],error='The star check could not obtain complete observations; trained weights are preserved.',**kwargs) def run_native(track_bytes,network_bytes,*,binary=None,importer=None,scene_template=None,with_report=False,gpu_library=None,config=None): started=time.perf_counter() config=validate_config(config, work_limit=8_000_000 if gpu_library is not None else 4_000_000) track=track_input(track_bytes); template=network_input(network_bytes) if config['mode']=='train':check_freeze_shape(config['freeze'],template['NeuralNetwork']['Shape']) # Deployment paths are supplied by trusted application configuration, never uploads. binary=Path(binary or ROOT/'target/release/altd-sim').resolve() importer=Path(importer or ROOT/'target/release/examples/extract_saved_track').resolve() scene_template=vehicle_scene_template(scene_template,template['Type']) if not all(p.is_file() for p in (binary,importer,scene_template)): raise RuntimeError('Native tools or scene template unavailable') # No inherited provider tokens, credential paths, or caller-chosen commands. env=native_environment(gpu_library) with tempfile.TemporaryDirectory(prefix='tiga-native-') as directory: work=Path(directory) env.update(HOME=directory,TMPDIR=directory) def save(name,value): path=work/name; path.write_text(json.dumps(value,allow_nan=False)); return str(path) saved=save('track.json',track) scene=work/'scene.json' subprocess.run([str(importer),saved,'User track',str(scene_template),str(scene)], cwd=work,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10,check=True) imported=json.loads(scene.read_text()) spawn=save('spawn.json',{'frames':[{'position':imported['reset_position'],'rotation':imported['reset_rotation']}]}) n=template['NeuralNetwork']; names=[f'sensor-{i}' for i in range(n['Shape'][0])] network=save('network.json',{'shape':n['Shape'],'weights':n['Weights'],'biases':n['Biases'],'inputs':names,'outputs':[OUTPUT_NAMES[i] for i in template['Outputs']]}) model=save('model.json',{'sensor_layout':{'names':names,'sensors':template['Sensors']}}) settings=save('settings.json',{'population':config['population'],'selection_algorithm':config['selection'],'selection_size':config['parents'], 'crossover':config['crossover'],'mutation_rate':config['mutation'],'adaptive_mutation':config['adaptive_mutation'],'weight_decay':config['decay'], 'preserve_parents':'on_custom','preserve_parents_size':config['elite'], 'rewards':config['rewards']}) out=work/'result' command=[str(binary),'--math-profile','proton','--threads',str(config['workers']),'train-scratch', '--scene',str(scene),'--spawn-trace',spawn,'--network',network,'--init-network',network, '--model',model,'--settings',settings,'--shape',','.join(map(str,n['Shape'])), '--population',str(config['population']),'--generations',str(config['generations']), '--ticks',str(config['ticks']),'--seed',str(config['seed']),'--batch-count',str(60//config['control_hz']), '--out-dir',str(out),'--mutation-start',str(config['mutation']), '--mutation-end',str(config['mutation'] if config['schedule']=='fixed' else config['mutation_end']), '--schedule','linear' if config['schedule']=='fixed' else config['schedule'],'--save-final-candidate', '--checkpoint-every','0','--analysis','--clean-min-laps',str(config['clean_min_laps']), '--eliminate-on-wall' if config['collision'] else '--no-eliminate-on-wall', '--idle-eliminate' if config['idle'] else '--no-idle-eliminate'] if config['mode']=='train' and any(config['freeze'].values()): command.extend(('--freeze-spec',save('freeze.json',config['freeze']))) if config['mode']=='evaluate':command.append('--evaluate-frozen') if gpu_library is not None: command.append('--gpu') subprocess.run(command,cwd=work,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=45 if gpu_library is not None else 25,check=True) if config['mode']=='train': candidate=out/'candidate.json' if candidate.stat().st_size>1024*1024: raise ValueError('Output too large') trained=json.loads(candidate.read_text()) if any(config['freeze'].values()):verify_frozen_parameters(config['freeze'],n,trained) n['Weights']=trained['weights']; n['Biases']=trained['biases'] result=(network_bytes if config['mode']=='evaluate' else gzip.compress(json.dumps(template,allow_nan=False).encode(),mtime=0)) network_input(result) history=[json.loads(line) for line in (out/'log.jsonl').read_text().splitlines() if line.strip()] last=history[-1] if history else {} files,summary,evaluation=assemble_analysis(out,template,result,network_bytes,track_bytes,config,'cuda' if gpu_library is not None else 'cpu',network_input) stars=check_current_network(work,command,env,track,template,config,result,track_bytes, 'cuda' if gpu_library is not None else 'cpu') files['stars.json']=json.dumps(stars,allow_nan=False,separators=(',',':')).encode() report={'mode':config['mode'],'analysis_summary':summary,'_analysis_files':files,'build':build_identity(),'state':'completed','elapsed_seconds':time.perf_counter()-started, 'generations_completed':len(history),'generations_requested':config['generations'],'population':config['population'], 'best_score':last.get('best_score'),'mean_score':last.get('mean_score'), 'best_lap_seconds':best_run_lap(history),'best_lap_scope':'Best completed lap observed during this run; may belong to a different car than the exported candidate','lapped_cars':last.get('lapped_cars'), 'simulated_ticks':last.get('simulated_ticks'),'requested_ticks':config['ticks'], 'candidate_validated':True,'execution_backend':'cuda' if gpu_library is not None else 'cpu', 'vehicle_type':template['Type'],'vehicle_name':VEHICLE_NAMES[template['Type']], 'inputs':{'track_sha256':hashlib.sha256(track_bytes).hexdigest(),'network_sha256':hashlib.sha256(network_bytes).hexdigest()}, 'settings':config,'history':[{'generation':i+1,'mutation':h.get('mutation_rate'), 'best_score':h.get('best_score'),'mean_score':h.get('mean_score'),'best_lap_seconds':h.get('best_lap_s'), 'lapped_cars':h.get('lapped_cars'),'simulated_ticks':h.get('simulated_ticks')} for i,h in enumerate(history)]} # The context has removed input/intermediate files before result publication. return (result,report) if with_report else result def run_reported(track_bytes,network_bytes,config=None): return run_native(track_bytes,network_bytes,with_report=True,config=config)