Verify bounded ordinary-input approach to the prison gate

This commit is contained in:
2026-09-19 19:21:21 +02:00
parent b7fba094d7
commit ec1071595d
90 changed files with 102832 additions and 35 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ def check():
assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries'
print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities')
print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla references')
for name in ['test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
for name in ['test_prison_approach_proof.py', 'test_prison_approach_evidence.py', 'test_ordinary_gate_evidence.py', 'test_ordinary_gate_observer.py', 'test_ordinary_look_observer.py', 'test_ordinary_look_evidence.py', 'test_relative_look_runner.py', 'test_radio_waiting_text.py', 'test_inventory_control_runner.py', 'test_inventory_layout.py', 'test_headless_input_proof.py', 'test_headless_input_exercise_runner.py', 'test_private_prefix_cleanup.py', 'test_runner_private_cleanup.py', 'test_ordinary_input_runner.py', 'test_ordinary_input_observer.py', 'test_aircraft_route_envelope.py', 'test_aircraft_route_policy.py', 'test_aircraft_route_probe.py', 'test_rough_intel_feedback.py', 'test_headless_display_probe.py', 'test_headless_display_runner.py', 'test_preparation_status.py', 'test_hint_delivery.py', 'test_native_end_lifecycle.py', 'test_native_end_runner.py', 'test_escape_result_wording.py', 'test_native_stats.py', 'test_aircraft_translation.py', 'test_spawn_retry_budget.py', 'test_action_feedback.py', 'test_native_aircraft_control.py', 'test_aircraft_flight_policy.py', 'test_aircraft_flight_envelope.py', 'test_ordinary_screenshot.py', 'test_participant_selection.py', 'test_native_shared_map_ownership.py', 'test_shared_map_ownership.py', 'test_intel_registration.py', 'test_mortar_activity_completion.py', 'test_packed_chat_game.py', 'test_native_mortar_refresh.py', 'test_archive_packed.py', 'test_mortar_refresh.py', 'test_packed_core_game.py', 'test_pak_inventory.py', 'test_native_mortar_salvo.py', 'test_spectator_guidance.py', 'test_native_package.py', 'test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_flight_action_runner.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True)
print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.')
if __name__=='__main__':check()
+66 -13
View File
@@ -348,13 +348,15 @@ def validate_paths(root, steam, compat, profile):
refuse(f'Unexpected profile symlink: {path}')
def command_for(root, steam, compat, profile, project, packed=None, capture_prison_view=False):
def command_for(root, steam, compat, profile, project, packed=None, capture_prison_view=False, private_width=1280):
if project not in {'core', 'chat', CONTROL}:
refuse('Unsupported ordinary game project/control')
if capture_prison_view and (project == CONTROL or packed is not None):
refuse('Prison view capture requires loose successor source with its opt-in observer')
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720',
'-w', '1280', '-h', '720', '-r', '30', '--',
if type(private_width) is not int or private_width not in (1280, 2560) or (private_width != 1280 and (project == CONTROL or packed is not None)):
refuse('Unsupported private headless viewport')
command = ['gamescope', '--backend', 'headless', '-W', str(private_width), '-H', '720',
'-w', str(private_width), '-h', '720', '-r', '30', '--',
str(steam/'steamapps/common/Proton - Experimental/proton'), 'run',
str(steam/'steamapps/common/Arma Reforger/ArmaReforgerSteam.exe')]
if packed is not None:
@@ -633,6 +635,10 @@ def validate_input_observer(a):
def validate_input_exercise(a):
if getattr(a, 'approach_wide_display', False) and not getattr(a, 'exercise_prison_approach', False):
refuse('Wide private viewport requires the explicit prison approach experiment')
if getattr(a, 'exercise_prison_approach', False) and not (getattr(a, 'exercise_relative_look', False) and getattr(a, 'observe_prison_gate', False)):
refuse('Prison approach requires explicit relative look and gate observation')
if getattr(a, 'exercise_relative_look', False) and not getattr(a, 'exercise_player_input', False):
refuse('Relative look requires the existing explicit keyboard exercise')
if not getattr(a, 'exercise_player_input', False):
@@ -832,7 +838,17 @@ def input_observer_result(result, content, nonce):
return result, evidence
def input_exercise_command(command, root, compat, profile, executable, run_id, host_display, display_context, look=False):
def approach_module_hashes(root):
hashes = {}
for name in ('prison_approach_proof.py', 'ordinary_gate_evidence.py', 'ordinary_look_evidence.py', 'prison_approach_evidence.py'):
path = root/'tools'/name
if path.is_symlink() or not path.is_file():
refuse('Approach requires regular local proof modules')
hashes[name] = hashlib.sha256(path.read_bytes()).hexdigest()
return hashes
def input_exercise_command(command, root, compat, profile, executable, run_id, host_display, display_context, look=False, approach=False):
"""Put the input supervisor around the display inspector, inside Gamescope."""
separator = command.index('--')
if command[:separator].count('--backend') != 1 or command[command.index('--backend')+1] != 'headless':
@@ -843,6 +859,12 @@ def input_exercise_command(command, root, compat, profile, executable, run_id, h
if (not display_context or len(child) < 3 or child[:2] != [sys.executable, str(ownership)]
or not re.fullmatch(r'[A-Za-z0-9]{1,32}', run_id)):
refuse('Input exercise requires the existing scoped display wrapper and observer nonce')
if type(approach) is not bool or (approach and not look):
refuse('Prison approach requires explicit relative look')
if approach:
indices = [i for i,value in enumerate(child) if value.startswith('-errOrdinaryGate')]
if len(indices) != 1 or child[indices[0]] != '-errOrdinaryGateObserve' or child[indices[0]+1:indices[0]+2] != [run_id]:
refuse('Prison approach requires its exact native gate observer nonce')
if type(look) is not bool:
refuse('Relative look selection must be boolean')
look_options = [i for i,value in enumerate(child) if value.startswith('-errOrdinaryLook')]
@@ -864,6 +886,10 @@ def input_exercise_command(command, root, compat, profile, executable, run_id, h
if look:
context['look_requested'] = True
wrapper.insert(-1, '--enable-private-look-proof')
if approach:
context['approach_requested'] = True
context['approach_module_sha256'] = approach_module_hashes(root)
wrapper.insert(-1, '--enable-private-approach-proof')
return command[:separator+1] + wrapper + child, context
@@ -885,6 +911,10 @@ def input_exercise_poll_failure(context):
for key in ('helper_sha256', 'ownership_helper_sha256', 'prefix', 'profile', 'game_executable', 'report', 'command', 'nonce'):
if report.get(key) != context[key]:
return False
if context.get('approach_requested') and report.get('approach_module_sha256') != context.get('approach_module_sha256'):
return None
if report.get('approach_requested', False) != context.get('approach_requested', False):
return None
if report.get('look_requested', False) != context.get('look_requested', False):
return False
return bool(report.get('error') or report.get('supervision_failed') is True or (
@@ -928,6 +958,13 @@ def input_exercise_result(result, context, session, observation, display_result)
for key in ('helper_sha256', 'ownership_helper_sha256', 'prefix', 'profile', 'game_executable', 'report', 'command', 'nonce'):
if report.get(key) != context[key]:
refuse('Input report binding mismatch: '+key)
approach_requested = context.get('approach_requested', False)
if type(report.get('approach_requested', False)) is not bool or report.get('approach_requested', False) != approach_requested:
refuse('Prison approach request does not match launch context')
if approach_requested and (report.get('approach_module_sha256') != context.get('approach_module_sha256') or context.get('approach_module_sha256') != approach_module_hashes(ROOT)):
refuse('Approach module sources changed or differ from launch hashes')
if not approach_requested and report.get('approach_proof') is not None:
refuse('Unexpected approach evidence in fixed input run')
look_requested = context.get('look_requested', False)
if type(report.get('look_requested', False)) is not bool or report.get('look_requested', False) != look_requested:
refuse('Relative look request does not match launch context')
@@ -944,11 +981,15 @@ def input_exercise_result(result, context, session, observation, display_result)
import math
events = report.get('events', [])
expected = [('W', True), ('W', False), ('Tab', True), ('Tab', False), ('Tab', True), ('Tab', False)]
if approach_requested:
if not isinstance(events, list) or not 2 <= len(events) <= 24 or len(events)%2:
refuse('Prison approach requires bounded W pairs')
expected = [('W', i%2 == 0) for i in range(len(events))]
if (not isinstance(events, list) or len(events) != len(expected)
or any(not isinstance(e, dict) or type(e.get('down')) is not bool or (e.get('key'), e['down']) != pair or e.get('finally') for e, pair in zip(events, expected))):
refuse('Input experiment does not contain the exact fixed key pairs')
times = [e.get('monotonic') for e in events]
if any(type(t) not in {int, float} or not math.isfinite(t) or t < 0 for t in times) or times != sorted(times) or times[-1]-times[0] > 25:
if any(type(t) not in {int, float} or not math.isfinite(t) or t < 0 for t in times) or times != sorted(times) or times[-1]-times[0] > (35 if approach_requested else 25):
refuse('Input event clock is invalid or outside its bound')
if not display_result or display_result.get('verified') is not True:
refuse('Input experiment lacks verified private display ownership')
@@ -958,15 +999,25 @@ def input_exercise_result(result, context, session, observation, display_result)
refuse('Input owner does not match the independently observed private game window')
native = report.get('observer', {})
if (report.get('console') != str(session/'console.log')
or native.get('events') != ['READY', 'INVENTORY_OPEN', 'INVENTORY_CLOSED']):
or native.get('events') != (['READY'] if approach_requested else ['READY', 'INVENTORY_OPEN', 'INVENTORY_CLOSED'])):
refuse('Input experiment lacks the exact native session/transition handshake')
if (not observation or observation.get('nonce') != context['nonce'] or not observation.get('sampler_complete')
or not observation.get('inventory_open_observed') or not observation.get('inventory_close_observed')
or not observation.get('movement', {}).get('action_observed')
or (not approach_requested and (not observation.get('inventory_open_observed') or not observation.get('inventory_close_observed')))
or (not approach_requested and (not observation.get('movement', {}).get('action_observed')
or not observation.get('movement', {}).get('physical_motion_observed')
or not observation.get('movement', {}).get('released_input_observed')):
or not observation.get('movement', {}).get('released_input_observed')))):
refuse('Native movement and inventory open/close proof is incomplete')
if look_requested:
if approach_requested:
from prison_approach_evidence import validate_approach_evidence
route = report.get('approach_proof', {})
route_events = route.get('events', [])
if len(route_events) != len(events) or any(not isinstance(a, dict) or a.get('key') != b.get('key') or a.get('down') is not b.get('down') or type(a.get('monotonic')) not in (int, float) or not math.isfinite(a['monotonic']) or abs(a['monotonic']-b['monotonic']) > .01 for a,b in zip(route_events,events)):
refuse('Approach adapter and route key evidence differ')
proof['approach_proof'] = validate_approach_evidence((session/'console.log').read_text(errors='replace'), context['nonce'], report.get('look_proof'), route)
proof['scope'] = 'private relative mouse and W input with independently observed interior prison approach; no held action, gate exit, full escape or multiplayer qualification'
if proof['approach_proof'].get('verified') is not True:
refuse('Independent native prison approach evidence is incomplete')
elif look_requested:
from ordinary_look_evidence import validate_look_evidence
proof['look_proof'] = validate_look_evidence((session/'console.log').read_text(errors='replace'), context['nonce'], report.get('look_proof'), events)
proof['scope'] = 'bounded private relative mouse and keyboard delivery with native view/movement/inventory evidence; no gate, full escape or multiplayer qualification'
@@ -1013,7 +1064,7 @@ def run(a):
def run_locked(a, steam, compat, profile, packed=None):
command = command_for(ROOT, steam, compat, profile, a.project, packed, getattr(a, 'capture_prison_view', False))
command = command_for(ROOT, steam, compat, profile, a.project, packed, getattr(a, 'capture_prison_view', False), 2560 if getattr(a, 'approach_wide_display', False) else 1280)
separator = command.index("--")
proton, executable = Path(command[separator+1]), Path(command[separator+3])
if not proton.is_file() or not executable.is_file():
@@ -1047,7 +1098,7 @@ def run_locked(a, steam, compat, profile, packed=None):
command, display_context = display_probe_command(command, ROOT, compat, profile, executable, run_id, os.environ.get('DISPLAY', ''))
exercise_context = None
if getattr(a, 'exercise_player_input', False):
command, exercise_context = input_exercise_command(command, ROOT, compat, profile, executable, run_id, os.environ.get('DISPLAY', ''), display_context, getattr(a, 'exercise_relative_look', False))
command, exercise_context = input_exercise_command(command, ROOT, compat, profile, executable, run_id, os.environ.get('DISPLAY', ''), display_context, getattr(a, 'exercise_relative_look', False), getattr(a, 'exercise_prison_approach', False))
silent_sink = 'erri_game_'+run_id
application_id = 'org.escapistsrereintroduced.game.'+run_id
env = os.environ.copy()
@@ -1173,7 +1224,7 @@ def run_locked(a, steam, compat, profile, packed=None):
'failure_observed_during_polling': monitor.first_failure_at is not None,
'first_failure_elapsed_seconds': (monitor.first_failure_at - monitor.started) if monitor.first_failure_at is not None else None,
'executable': str(executable), 'dedicated_server': False,
'display': 'headless Gamescope', 'audio': '-noSound plus own silent sink',
'display': 'headless Gamescope', 'private_viewport': {'width':2560 if getattr(a, 'approach_wide_display', False) else 1280,'height':720}, 'audio': '-noSound plus own silent sink',
'control_only': a.project == CONTROL,
'source_manifest_scope': 'none: packed input provenance is separate; loose source manifests cannot qualify this run' if packed else 'successor addon snapshot retained for provenance bookkeeping only; none loaded by vanilla control' if a.project == CONTROL else 'successor addon sources',
'scope': final.get('scope', 'ordinary game compile/startup; no scripted fixture, full escape or multiplayer acceptance')}
@@ -1211,6 +1262,8 @@ def main():
parser.add_argument('--inspect-headless-display', action='store_true', help='Read-only ownership inspection within the hidden display; no input sent, loose Core/Chat only')
parser.add_argument('--capture-prison-view', action='store_true', help='Opt-in single genuine local prison screenshot; loose successor source only, no camera or input changes')
parser.add_argument('--observe-player-input', action='store_true', help='Read-only native local-input sampler; loose Core/Chat only, requires --settle >=50; sends no input')
parser.add_argument('--approach-wide-display', action='store_true', help='Use a disposable 2560x720 headless viewport for the explicit approach experiment; no physical display changes')
parser.add_argument('--exercise-prison-approach', action='store_true', help='Explicit bounded relative-mouse/W approach inside prison; requires relative look and gate observer; never operates gate')
parser.add_argument('--observe-prison-gate', action='store_true', help='Read-only native registered prison lock/context/progress telemetry; requires --observe-player-input')
parser.add_argument('--exercise-relative-look', action='store_true', help='Explicit tiny relative right/return mouse experiment before W/Tab; requires --exercise-player-input and native view proof')
parser.add_argument('--exercise-player-input', action='store_true', help='Explicit bounded private W/Tab keyboard experiment; requires observer, display inspection, loose Core/Chat and settle >=50')
+220 -12
View File
@@ -51,6 +51,8 @@ def game_arguments(owner, args):
raise Refused('Native observer nonce mismatch')
if getattr(args, 'enable_private_look_proof', False) and option(native['args'], '-errOrdinaryLookObserve') != args.nonce:
raise Refused('Native look observer nonce mismatch')
if getattr(args, 'enable_private_approach_proof', False) and option(native['args'], '-errOrdinaryGateObserve') != args.nonce:
raise Refused('Native gate observer nonce mismatch')
if option(native['args'], '-errOrdinaryScenario') not in ('core', 'chat'):
raise Refused('Not an ordinary successor scenario')
if option(native['args'], '-profile') != 'Z:' + str(args.profile):
@@ -115,8 +117,9 @@ def yaw_delta(first, second):
class RelativeMouse:
def __init__(self, query, keyboard, owner):
def __init__(self, query, keyboard, owner, enable_approach=False):
self.query, self.keyboard, self.owner = query, keyboard, owner
self.enable_approach = enable_approach
self.xt = keyboard.xt
self.xt.XTestFakeRelativeMotionEvent.restype = C.c_int
self.xt.XTestFakeRelativeMotionEvent.argtypes = [C.c_void_p, C.c_int, C.c_int, C.c_ulong]
@@ -148,12 +151,27 @@ class RelativeMouse:
def move(self, dx, dy, guard):
if type(dx) is not int or dx not in (-24, 24) or dy != 0 or type(dy) is not int:
raise Refused('Unsupported relative look step')
self._move(dx, dy, guard)
def approach_move(self, dx, dy, guard, *, deadline, clock=time.monotonic):
if (self.enable_approach is not True or type(dx) is not int or not 1 <= abs(dx) <= 128
or type(dy) is not int or dy != 0):
raise Refused('Unsupported or unenabled approach relative step')
def within_lease():
if clock() >= deadline:
raise Refused("Original approach deadline exceeded before relative input")
within_lease()
self._move(dx, dy, guard, before_send=within_lease)
def _move(self, dx, dy, guard, before_send=None):
target = guard.check()
self.keyboard.require_idle()
self.validate_pointer(target, dx, dy)
# Recheck process/focus after the pointer roundtrip, with no focus changes.
target = guard.check()
self.validate_pointer(target, dx, dy)
if before_send is not None:
before_send()
if not self.xt.XTestFakeRelativeMotionEvent(self.query.d, dx, dy, 0):
raise Refused('Native relative motion request failed')
self.query.x.XFlush(self.query.d)
@@ -252,7 +270,7 @@ class Observer:
"""Only records appended after this exact owned console was opened count."""
pattern = re.compile(r'ERR INPUT OBSERVE: run=([A-Za-z0-9]{1,32}) sample=(\d+) real=(\d+) world=(\d+(?:\.\d+)?) (.+)$')
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False):
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False, enable_approach=False):
self.path, self.nonce, self.clock = path, nonce, clock
self.fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
info = os.fstat(self.fd)
@@ -269,7 +287,14 @@ class Observer:
self.raw = []
self.bytes = 0
self.terminal = False
if enable_approach and not enable_look:
os.close(self.fd)
raise Refused("Approach observation requires native look")
self.enable_look = enable_look
self.enable_approach = enable_approach
self.approach_lines = []; self.gate_records = []; self.gate_received = {}
self.gate_complete_end = 0; self.approach_previous = None
self.ready_at = self.ready_real = self.queued_real = None
self.look_records = []; self.input_samples = []; self.ready_sample = None
def close(self):
@@ -289,6 +314,20 @@ class Observer:
lines = self.pending.split(b'\n'); self.pending = lines.pop()
for raw in lines:
line = raw.decode('utf-8', 'strict').rstrip('\r')
if self.enable_approach and any(prefix in line for prefix in ('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:')):
if len(self.approach_lines) >= 1900:
raise Refused('Native approach record bound exceeded')
self.approach_lines.append(line)
if self.enable_approach and 'ERR GATE OBSERVE:' in line:
from ordinary_gate_evidence import parse_gate_record
record = parse_gate_record(line, self.nonce)
self.gate_records.append(record)
if len(self.gate_records) > 902:
raise Refused('Native gate record bound exceeded')
if record['kind'] == 'geometry':
self.gate_complete_end = len(self.approach_lines)
self.gate_received.setdefault(record['sample'], self.clock())
continue
if self.enable_look and 'ERR LOOK OBSERVE:' in line:
record = parse_look_record(line, self.nonce)
if len(self.look_records) >= 453:
@@ -326,11 +365,16 @@ class Observer:
self.input_samples.append(parse_stationary_sample(sample, real, detail))
self.sample, self.real, self.world = sample, real, world
self.raw.append(line)
if self.enable_approach and detail == 'QUEUED':
if self.queued_real is not None:
raise Refused('Repeated native queued clock')
self.queued_real = real
if detail == 'READY':
if self.events or self.last_sample_at is None:
raise Refused('Repeated or unbound READY')
self.events.append('READY')
self.ready_sample = sample
self.ready_at = self.clock(); self.ready_real = real
elif detail in ('INVENTORY_OPEN', 'INVENTORY_CLOSED'):
expected = ['READY'] if detail == 'INVENTORY_OPEN' else ['READY', 'INVENTORY_OPEN']
if self.events != expected:
@@ -347,6 +391,47 @@ class Observer:
raise Refused('Native observer samples are stale')
def approach_data(self):
"""Reparse complete raw triples, retaining incomplete final pairs for next poll."""
from ordinary_gate_evidence import join_gate_snapshots, validate_gate_geometry
self.fresh()
if (not self.enable_approach or self.events != ['READY'] or not self.gate_complete_end or
any(r['kind'] == 'cancelled' for r in self.gate_records + self.look_records)):
raise Refused('Native approach context is unavailable or ended')
content = '\n'.join(self.approach_lines[:self.gate_complete_end])
data = join_gate_snapshots(content, self.nonce)
index = max(data['snapshots'])
geometry = validate_gate_geometry(content, self.nonce, data['ready']['sample'], index)
if not geometry['verified']:
raise Refused('Native gate geometry refused: ' + '; '.join(geometry['errors']))
for item in data['snapshots'].values():
action = item['action']
if item['sample'] >= data['ready']['sample'] and (action['pressed'] != 0 or action['performing'] or action['progress'] != 0):
raise Refused('Gate action input or progress changed during approach-only observation')
if (self.clock() - self.gate_received[index] > .5 or
self.input_samples[-1]['real_ms'] - data['snapshots'][index]['real_ms'] > 500):
raise Refused('Complete native gate snapshot is stale')
return content, data
def approach_snapshots(self):
_, data = self.approach_data()
snapshots = data['snapshots']; latest = max(snapshots)
first = latest if self.approach_previous is None else min(self.approach_previous + 1, latest)
if self.approach_previous is not None and latest < self.approach_previous:
raise Refused('Native approach snapshot index reversed')
result = []
for index in range(first, latest + 1):
item = snapshots[index]; geo, action, native, pose = item['geometry'], item['action'], item['input'], item['look']
result.append({'sample': index, 'real_ms': item['real_ms'], 'received_monotonic': self.gate_received[index],
'origin': native['origin'], 'direction': pose['direction'], 'body_forward': pose['body_forward'],
'input': native['input'], 'forward': native['forward'], 'velocity': native['velocity'],
'inventory': native['inventory'], 'phase': action['phase'], 'door_state': geo['door_state'], 'door_control': geo['door_control'],
'geometry': {'nonce': self.nonce, 'basis': 'registered-prison-lock-native-interaction',
**{key: geo[key] for key in ('prison', 'gate', 'gate_forward', 'context', 'radius')}}})
self.approach_previous = latest
return result
class Keyboard:
"""Only W/Tab key events on the pre-existing explicit owned connection."""
def __init__(self, query):
@@ -439,7 +524,7 @@ def confirm_release(guard, keyboard, report, key, clock=time.monotonic, sleep=ti
observation['end'] = clock()
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None):
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False):
deadline = clock() + 70
held = None
report.update(events=[], paired_release_errors=[], sequence_completed=False)
@@ -493,11 +578,26 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
try:
wait_for('READY', 45)
deadline = clock() + 12
if approach:
if mouse is None or not observer.enable_approach or observer.ready_at is None:
raise Refused('Approach requires explicit look and gate observation')
deadline = observer.ready_at + 35
if clock() >= deadline:
raise Refused('Original approach deadline exceeded before calibration')
notify({'kind': 'approach-pending', 'deadline_monotonic': deadline})
if observer.events != ['READY']:
raise Refused('Inventory was already changed before input')
if mouse is not None:
perform_look(guard, keyboard, observer, mouse, report, clock, sleep)
deadline = clock() + 12
if not approach:
deadline = clock() + 12
if approach:
run_approach(guard, keyboard, observer, mouse, report, clock, sleep)
if clock() >= deadline:
raise Refused("Original approach deadline exceeded")
keyboard.require_idle()
report["sequence_completed"] = True
return
pulse('W', 0.30)
check()
pulse('Tab', 0.05)
@@ -526,20 +626,86 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
except (OSError, ValueError) as error:
report['paired_release_errors'].append(str(error))
report['unconfirmed_held_key'] = held if report['paired_release_errors'] else None
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key')
if approach:
route = report.get('approach_proof', {})
report['paired_release_errors'].extend(route.get('paired_release_errors', []))
report['unconfirmed_held_key'] = route.get('unconfirmed_held_key')
notify({'kind': 'approach-finished', 'successful': report['sequence_completed']})
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key') and not report['paired_release_errors']
class ApproachKeyboard:
"""W-only capability for the separate approach module; never F/Tab."""
def __init__(self, keyboard, report, clock, deadline):
self.keyboard, self.report, self.clock, self.deadline = keyboard, report, clock, deadline
def require_idle(self):
self.keyboard.require_idle()
def send(self, key, down):
if key != 'W' or type(down) is not bool:
raise Refused('Approach permits W key pairs only')
if down and self.clock() >= self.deadline:
raise Refused('Original approach deadline exceeded before W press')
self.report['input_attempted'] = True
self.keyboard.send(key, down)
self.report['events'].append({'key': key, 'down': down, 'monotonic': self.clock()})
self.report['input_sent'] = True
class ApproachMouse:
def __init__(self, mouse, report, clock, deadline):
self.mouse, self.report, self.clock, self.deadline = mouse, report, clock, deadline
def move(self, dx, dy, guard):
self.report['input_attempted'] = True
self.mouse.approach_move(dx, dy, guard, deadline=self.deadline, clock=self.clock)
self.report['input_sent'] = True
def run_approach(guard, keyboard, observer, mouse, report, clock, sleep):
from ordinary_look_evidence import validate_look_evidence
from prison_approach_proof import perform_approach
content, data = observer.approach_data()
validated = validate_look_evidence(content, observer.nonce, report['look_proof'])
if not validated['verified']:
raise Refused('Native approach calibration refused: ' + '; '.join(validated['errors']))
poses = {r['sample']: r for r in observer.look_records if r['kind'] == 'sample'}
proof = report['look_proof']
if observer.queued_real is None or observer.ready_at is None or observer.ready_real is None:
raise Refused('Original native observation clocks missing')
calibration = {'nonce': observer.nonce, 'completed': True, 'pixels': 24,
'baseline_direction': poses[proof['baseline_samples'][-1]]['direction'],
'response_direction': poses[proof['response_sample']]['direction'],
'ready_real_ms': observer.ready_real, 'ready_monotonic': observer.ready_at,
'observer_start_real_ms': observer.queued_real}
geometry = observer.approach_snapshots()[-1]['geometry']
# The adapter has only read here. The module's initial snapshot can repeat
# this latest sample; every subsequent native sample is returned in order.
def route_notify(event):
# Only the sequence owner can mark completion after its final checks.
if event.get("kind") != "approach-finished":
notify(event)
deadline = observer.ready_at + 35
perform_approach(guard, ApproachKeyboard(keyboard, report, clock, deadline), ApproachMouse(mouse, report, clock, deadline),
observer.approach_snapshots, geometry, calibration, report,
clock=clock, sleep=sleep, notify=route_notify, key_state_busy=KeyStateBusy)
if not report.get('approach_proof', {}).get('completed'):
raise Refused('Approach module did not complete')
def arguments(argv=None):
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--enable-private-keyboard-proof', action='store_true', required=True)
p.add_argument('--enable-private-look-proof', action='store_true')
p.add_argument('--enable-private-approach-proof', action='store_true', help='After look calibration, replace W/Tab cycle with bounded interior approach only')
for name in ('prefix', 'game-executable', 'profile', 'report'):
p.add_argument('--' + name, type=Path, required=True)
p.add_argument('--console', type=Path, help='Existing exact session; omit only in wrapper mode')
p.add_argument('--host-display', required=True)
p.add_argument('--nonce', required=True)
p.add_argument('command', nargs=argparse.REMAINDER)
return p.parse_args(argv)
args = p.parse_args(argv)
if args.enable_private_approach_proof and not args.enable_private_look_proof:
p.error("Approach proof requires explicit private look proof")
return args
def wrapper_command(args):
@@ -569,6 +735,8 @@ def wrapper_command(args):
raise Refused('Wrapped native observer context differs')
if args.enable_private_look_proof and option(native, '-errOrdinaryLookObserve') != args.nonce:
raise Refused('Wrapped native look observer context differs')
if getattr(args, 'enable_private_approach_proof', False) and option(native, '-errOrdinaryGateObserve') != args.nonce:
raise Refused('Wrapped native gate observer context differs')
return command
@@ -589,6 +757,31 @@ def console_path(args):
raise Refused('Expected exact canonical native session console')
APPROACH_MODULES = ('prison_approach_proof.py', 'ordinary_gate_evidence.py',
'ordinary_look_evidence.py', 'prison_approach_evidence.py')
def approach_module_hashes():
folder = Path(__file__).resolve().parent
return {name: hashlib.sha256((folder / name).read_bytes()).hexdigest() for name in APPROACH_MODULES}
def report_for_write(report):
"""Keep raw console as pose authority; archive bounded references, not duplicate poses."""
if report.get('approach_requested') is not True:
return report
result = dict(report)
route = report.get('approach_proof')
if isinstance(route, dict):
result['approach_proof'] = dict(route)
samples = route.get('samples')
if isinstance(samples, list):
result['approach_proof']['samples'] = [
{key: sample.get(key) for key in ('sample', 'real_ms', 'received_monotonic')}
if isinstance(sample, dict) else {'invalid_reference': True} for sample in samples]
return result
def main(argv=None):
args = arguments(argv)
args.observe_seconds = 25; args.query_worker = False
@@ -601,11 +794,14 @@ def main(argv=None):
bound = display.binding(args.host_display, args.prefix)
report = {'schema': 1, 'kind': 'headless-private-keyboard-experiment', 'input_sent': False,
'look_requested': args.enable_private_look_proof,
'approach_requested': getattr(args, 'enable_private_approach_proof', False),
'sequence_completed': False, 'release_qualified': False, 'gameplay_qualified': False,
'hard_supervision': NOTIFY is not None, 'nonce': args.nonce, 'binding': bound, 'command': command, 'report': str(args.report), 'input_attempted': False,
'helper_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'ownership_helper_sha256': hashlib.sha256(Path(display.__file__).read_bytes()).hexdigest(),
'prefix': str(args.prefix), 'profile': str(args.profile), 'game_executable': str(args.game_executable)}
if report['approach_requested']:
report['approach_module_sha256'] = approach_module_hashes()
fd = os.open(args.report, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
info = os.fstat(fd)
notify({'kind': 'report', 'path': str(args.report), 'device': info.st_dev, 'inode': info.st_ino,
@@ -642,7 +838,8 @@ def main(argv=None):
if args.console is None:
raise Refused('New native console deadline exceeded')
report['console'] = str(args.console)
observer = Observer(args.console, args.nonce, enable_look=args.enable_private_look_proof)
observer = Observer(args.console, args.nonce, enable_look=args.enable_private_look_proof,
enable_approach=getattr(args, "enable_private_approach_proof", False))
query = display.X11(bound['display'])
while time.monotonic() < deadline:
if child is not None and child.poll() is not None:
@@ -669,8 +866,8 @@ def main(argv=None):
guard.check()
keyboard = Keyboard(query)
keyboard.require_idle()
mouse = RelativeMouse(query, keyboard, owner) if args.enable_private_look_proof else None
sequence(guard, keyboard, observer, report, mouse=mouse)
mouse = RelativeMouse(query, keyboard, owner, enable_approach=getattr(args, "enable_private_approach_proof", False)) if args.enable_private_look_proof else None
sequence(guard, keyboard, observer, report, mouse=mouse, approach=getattr(args, "enable_private_approach_proof", False))
except (OSError, ValueError, UnicodeError, subprocess.SubprocessError) as error:
report['error'] = str(error)
finally:
@@ -680,12 +877,14 @@ def main(argv=None):
'end_offset': observer.offset, 'records': observer.raw, 'events': observer.events}
if args.enable_private_look_proof:
report['look_observation'] = observer.look_records
if getattr(args, "enable_private_approach_proof", False):
report["gate_observation"] = observer.gate_records
observer.close()
if query:
query.close()
with os.fdopen(fd, 'w') as stream:
fd = None
json.dump(report, stream, indent=2); stream.write('\n'); stream.flush(); os.fsync(stream.fileno())
json.dump(report_for_write(report), stream, indent=2); stream.write('\n'); stream.flush(); os.fsync(stream.fileno())
notify({'kind': 'proof-finished', 'successful': report['sequence_completed'] and not report.get('error')})
child_code = 0
try:
@@ -773,6 +972,7 @@ def supervise(argv=None):
os.kill(child.pid, signum)
for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
previous[signum] = signal.signal(signum, forward)
approach_deadline = None; approach_finished = False
look_at = None; held_at = None; held_key = None; owned_child = None; report_record = None; proof_finished = False; expired = False
deadline = time.monotonic() + 190
try:
@@ -783,7 +983,15 @@ def supervise(argv=None):
event = parent.recv()
except EOFError:
event = None
if event and event['kind'] == 'look-pending':
if event and event['kind'] == 'approach-pending':
proposed = event.get('deadline_monotonic')
current = time.monotonic()
if type(proposed) not in (int, float) or not math.isfinite(proposed) or proposed > current + 35:
proposed = current # malformed lease fails closed
approach_deadline = proposed if approach_deadline is None else min(approach_deadline, proposed)
elif event and event['kind'] == 'approach-finished':
approach_finished = bool(event.get('successful'))
elif event and event['kind'] == 'look-pending':
look_at = time.monotonic()
elif event and event['kind'] == 'look-finished':
look_at = None
@@ -798,7 +1006,7 @@ def supervise(argv=None):
elif event and event['kind'] == 'proof-finished':
proof_finished = event['successful']
now = time.monotonic()
if received or now >= deadline or (held_at is not None and now - held_at > 1.5) or (look_at is not None and now - look_at > 4):
if received or now >= deadline or (approach_deadline is not None and not approach_finished and now >= approach_deadline) or (held_at is not None and now - held_at > 1.5) or (look_at is not None and now - look_at > 4):
expired = True
child.terminate() # asks worker finally to release if still provable
child.join(1)
+129
View File
@@ -0,0 +1,129 @@
"""Independent route qualification from raw native records; never sends input."""
import math
from ordinary_gate_evidence import join_gate_snapshots, validate_gate_geometry
from ordinary_look_evidence import validate_look_evidence
def need(value, message):
if not value:
raise ValueError(message)
def number(value):
need(type(value) in (int, float) and math.isfinite(value), 'invalid numeric evidence')
return value
def horizontal(a, b):
return math.hypot(a[0]-b[0], a[2]-b[2])
def norm(v):
return math.sqrt(sum(x*x for x in v))
def yaw(v):
return math.degrees(math.atan2(v[0], v[2]))
def angle(a, b):
return (a-b+180)%360-180
def neutral(s):
i=s['input']
return abs(i['forward']) <= .01 and norm(i['input']) <= .01 and norm(i['velocity']) <= .05
def validate_approach_evidence(content, nonce, look_proof, proof):
result={'verified':False,'errors':[], 'scope':'one private ordinary-input approach to the interior prison gate waypoint; no unlock, escape, visual, multiplayer or FPS qualification', 'held_action_verified':False,'escape_verified':False}
try:
need(type(proof) is dict and proof.get('requested') is True and proof.get('completed') is True and proof.get('nonce')==nonce, 'approach did not complete under this nonce')
need(not proof.get('error') and proof.get('paired_release_errors')==[] and proof.get('unconfirmed_held_key') is None, 'approach input did not end cleanly')
start=number(proof['started_monotonic']); finish=number(proof['finished_monotonic']); deadline=number(proof['deadline_monotonic'])
need(0 <= start < finish <= deadline and deadline-start <= 35, 'approach helper deadline invalid')
events=proof['events']; stops=proof['stops']; motions=proof['motions']; references=proof['samples']
need(type(events) is list and 2 <= len(events) <= 24 and len(events)%2==0, 'expected bounded W key pairs')
need(type(stops) is list and len(stops)*2==len(events), 'each W pair needs a native full-stop interval')
need(type(motions) is list and len(motions)<=16 and type(references) is list and 2 <= len(references) <= 450, 'route reference bounds invalid')
ids=[r['sample'] for r in references]
need(all(type(i) is int for i in ids) and ids==sorted(set(ids)), 'route sample references repeated or reversed')
data=join_gate_snapshots(content,nonce); samples=data['snapshots']; first,last=ids[0],ids[-1]
need(type(proof.get('final_sample')) is int and proof['final_sample']==last, 'final native reference differs from last sample')
need(first >= look_proof['return_sample'] and first in samples and last in samples, 'route is not after native look calibration')
receipts={}; previous_receipt=start-.5
for reference in references:
i=reference['sample']; need(i in samples and type(reference['real_ms']) is int and reference['real_ms']==samples[i]['real_ms'], 'helper sample clock differs from native source')
received=number(reference['received_monotonic'])
need(previous_receipt<=received<=finish, 'native receipt timestamps reversed or outside route')
receipts[i]=received; previous_receipt=received
geometry=validate_gate_geometry(content,nonce,first,last)
need(geometry['verified'], 'native route geometry rejected: '+str(geometry['errors']))
look=validate_look_evidence(content,nonce,look_proof,events[:2])
need(look['verified'], 'native calibration rejected: '+str(look['errors']))
need(start >= look_proof['finished_monotonic'], 'route started before calibration finished')
origin=samples[first]['geometry']; center=origin['prison']; hinge=origin['gate']; handle=origin['context']
dx,dz=center[0]-hinge[0],center[2]-hinge[2]; separation=math.hypot(dx,dz)
need(5.75 <= separation <= 6.25 and min(abs(dx),abs(dz)) <= .1, 'unsupported native prison geometry')
inward=(dx/separation,dz/separation)
need(abs(origin['gate_forward'][0]*inward[0]+origin['gate_forward'][2]*inward[1])>=.99 and horizontal(handle,hinge)<=4.5,'native gate orientation or handle invalid')
target=(handle[0]+1.5*inward[0],center[1],handle[2]+1.5*inward[1])
def inside(v): return abs(v[0]-center[0])<=5.25 and abs(v[2]-center[2])<=5.25 and abs(v[1]-center[1])<=3
need(inside(target), 'interior waypoint outside authored bounds')
ready=data['ready']; need(samples[last]['real_ms']-ready['real_ms']<35000,'native approach exceeded original READY bound')
path=0.; previous=None
for i in range(first,last+1):
need(i in samples,'missing raw route sample')
s=samples[i]; p=s['input']['origin']; g=s['geometry']; a=s['action']
need(inside(p) and norm(s['input']['velocity'])<=5,'body left interior motion envelope')
move=s['input']['input'];need(abs(move[0])<=.01 and abs(move[1])<=.01 and -.01<=move[2]<=1.01 and -.01<=s['input']['forward']<=1.01, 'unexpected native movement input')
need(g['door_state']==0 and g['door_control']==0 and a['pressed']==0 and not a['performing'] and a['progress']==0,'gate action or door was operated during approach')
if previous:
dt=s['real_ms']-previous['real_ms']; step=horizontal(p,previous['input']['origin'])
need(0<dt<=500 and step<=.25+5*dt/1000,'route contains a clock gap or discontinuous displacement')
path+=step
previous=s
need(path<=6 and .75<horizontal(samples[first]['input']['origin'],target)<=6,'native route path or initial distance outside scope')
need(horizontal(samples[last]['input']['origin'],target)<=.75 and neutral(samples[last]),'body did not stop at interior waypoint')
def stable_end(index):
need(index in samples and neutral(samples[index]),'native stop is not neutral')
end=samples[index]; cursor=index-1
while cursor>=first and end['real_ms']-samples[cursor]['real_ms']<100:
need(neutral(samples[cursor]),'native stop was not sustained');cursor-=1
need(cursor>=first and neutral(samples[cursor]) and horizontal(end['input']['origin'],samples[cursor]['input']['origin'])<=.02,'native stop lacks two stable samples across100ms')
need(abs(angle(yaw(end['look']['direction']),yaw(samples[cursor]['look']['direction'])))<=.2, 'native camera not settled at stop')
stable_end(last)
last_time=start; previous_stop=first; largest=None
for n,stop in enumerate(stops):
down,up=events[2*n:2*n+2]
need(down.get('key')=='W' and down.get('down') is True and up.get('key')=='W' and up.get('down') is False and not down.get('finally') and not up.get('finally'),'unexpected route key event')
td,tu=number(down['monotonic']),number(up['monotonic']);need(last_time<=td<tu<=finish and tu-td<=.4,'W hold/order outside route bound');last_time=tu
before,after=stop['before_sample'],stop['after_sample']
need(type(before) is int and type(after) is int and previous_stop<=before<after<=last,'invalid pulse native references');need(before in receipts and after in receipts and receipts[before]<=td<tu<=receipts[after] and td-receipts[before]<=.5, 'W timestamps contradict native receipt references');stable_end(before);stable_end(after)
begin,end=samples[before],samples[after]; a,b=begin['input']['origin'],end['input']['origin'];distance=horizontal(a,b)
need(.05<=distance<=1.25 and horizontal(a,target)-horizontal(b,target)>=.05,'native pulse missed or moved away')
need(horizontal(a,target)>(largest if largest is not None else 1.25)+.25,'native pulse lacked stopping margin')
wanted=math.degrees(math.atan2(target[0]-a[0],target[2]-a[2]));need(abs(angle(wanted,yaw(begin['look']['direction'])))<=3 and abs(angle(yaw(end['look']['direction']),yaw(begin['look']['direction'])))<=5,'pulse heading invalid')
need(any(samples[i]['input']['forward']>.01 and samples[i]['input']['input'][2]>.01 for i in range(before+1,after+1)),'W pulse has no observed forward input')
largest=max(largest or 0,distance);previous_stop=after
pixels=0;previous_motion=first;previous_motion_time=start;gain=look['response_yaw_degrees']/24
need(.01<=abs(gain)<=.5, 'native calibration gain outside approach range')
for motion in motions:
before,after=motion['after_sample'],motion['response_sample']; dx=motion['dx'];at=number(motion['monotonic'])
need(type(dx) is int and 1<=abs(dx)<=128 and type(motion['dy']) is int and motion['dy']==0,'relative step exceeds route bounds')
need(type(before) is int and type(after) is int and previous_motion<=before<after<=last and start<=at<=finish,'relative motion references/order invalid')
need(previous_motion_time<=at and before in receipts and after in receipts and receipts[before]<=at<=receipts[after] and at-receipts[before]<=.5, 'relative motion timestamps contradict native receipts')
previous_motion_time=at
a,b=samples[before],samples[after];change=angle(yaw(b['look']['direction']),yaw(a['look']['direction']))
need(.05<=abs(change)<=15 and change*dx*gain>0,'native turn did not acknowledge proposal')
for i in range(before,after+1):
need(neutral(samples[i]) and horizontal(samples[i]['input']['origin'],a['input']['origin'])<=.05,'body moved during heading correction')
turn=angle(yaw(samples[i]['look']['direction']),yaw(a['look']['direction']))
need(abs(turn)<=15 and (abs(turn)<.05 or turn*dx*gain>0), 'intermediate native turn exceeded bounds or reversed')
stable_end(after);pixels+=abs(dx);previous_motion=after
need(all(not (events[i]['monotonic'] <= m['monotonic'] <= events[i+1]['monotonic']) for m in motions for i in range(0,len(events),2)), 'mouse motion overlaps W hold')
need(pixels<=2048,'relative pixel sum exceeds route bound')
result.update(verified=True,first_sample=first,last_sample=last,native_duration_ms=samples[last]['real_ms']-samples[first]['real_ms'],path_metres=path,target=list(target),final_distance_metres=horizontal(samples[last]['input']['origin'],target),pulse_count=len(stops),turn_count=len(motions),geometry=geometry)
except (ValueError,KeyError,TypeError,OverflowError,IndexError) as error:
result['errors'].append(str(error))
return result
+307
View File
@@ -0,0 +1,307 @@
"""Bounded approach-only policy; no native library, display, process, or action calls.
Adapter contract (all records must be parsed from the same pinned console/nonce):
* observe() freshly joins INPUT/LOOK/GATE by sample+real_ms, validates the native
BASIS and absence of cancellation, and returns an ordered nonempty batch of
snapshots (all new consecutive samples, or repeat latest). Each has sample, real_ms,
received_monotonic, origin, direction, body_forward, input, forward, velocity,
inventory, phase, door_state, door_control, and geometry.
* geometry: nonce, basis, prison, gate, gate_forward, context, radius. Coordinates
are native GEOMETRY values; binding is the native observer's pinned references,
not a fabricated entity ID. This policy supports only the authored axis-aligned
12m enclosure; it derives an interior target from the observed handle.
* calibration is independently accepted same-run tiny look evidence projected to
nonce, completed, pixels, baseline_direction, response_direction,
ready_real_ms, ready_monotonic, observer_start_real_ms.
* guard.check() is the existing full private ownership/focus guard. keyboard has
require_idle()/send('W', bool). mouse.move(dx,0,guard) must use the same guarded
connection and current/projected clipped pointer checks, explicitly permitting
this variant's <=128px cap. Existing two-motion helper remains unchanged.
* notify receives the existing press-pending/release-confirmed events, plus
approach-pending (absolute monotonic deadline)/approach-finished. The wiring
MUST hard-supervise that deadline and the unchanged 1.5-second key lease.
* key_state_busy is the existing explicit Keyboard KeyStateBusy exception class;
other failures are never retried as busy-state polls.
No adapter implementation or input is executed by importing this module. A pass
proves an interior approach only, never reach, action selection, unlock or escape.
"""
import copy
import math
class Refused(ValueError):
pass
def require(value, reason):
if not value:
raise Refused(reason)
def number(value):
require(type(value) in (int, float) and math.isfinite(value), 'Nonfinite or invalid numeric observation')
return float(value)
def vector(value):
require(type(value) in (list, tuple) and len(value) == 3, 'Invalid vector observation')
result = tuple(number(v) for v in value)
require(all(abs(v) <= 10000000 for v in result), 'Unbounded native vector')
return result
def horizontal(a, b):
return math.hypot(a[0]-b[0], a[2]-b[2])
def length(value):
return math.sqrt(sum(v*v for v in value))
def yaw(direction):
d = vector(direction)
require(.95 <= length(d) <= 1.05 and math.hypot(d[0], d[2]) > .1, 'Invalid native direction')
return math.degrees(math.atan2(d[0], d[2]))
def delta(target, actual):
return (target-actual+180) % 360-180
def target_for(geometry):
require(type(geometry) is dict and set(geometry) == {'nonce', 'basis', 'prison', 'gate', 'gate_forward', 'context', 'radius'}, 'Incomplete native gate geometry')
require(type(geometry['nonce']) is str and 1 <= len(geometry['nonce']) <= 32 and geometry['nonce'].isascii() and geometry['nonce'].isalnum(), 'Invalid geometry nonce')
require(geometry['basis'] == 'registered-prison-lock-native-interaction', 'Missing pinned native gate basis')
prison, gate, context = (vector(geometry[k]) for k in ('prison', 'gate', 'context'))
facing = vector(geometry['gate_forward']); yaw(facing)
require(0 < number(geometry['radius']) <= 10, 'Invalid native context radius')
dx, dz = prison[0]-gate[0], prison[2]-gate[2]
distance = math.hypot(dx, dz)
require(5.75 <= distance <= 6.25 and min(abs(dx), abs(dz)) <= .1, 'Unsupported measured prison gate placement')
inward = (dx/distance, 0., dz/distance)
require(abs(facing[0]*inward[0]+facing[2]*inward[2]) >= .99, 'Gate orientation differs from authored enclosure')
require(horizontal(context, gate) <= 4.5, 'Context is not near original gate')
target = (context[0]+1.5*inward[0], prison[1], context[2]+1.5*inward[2])
require(inside(target, prison), 'Derived approach is outside safe prison interior')
return target, prison
def inside(position, center):
return abs(position[0]-center[0]) <= 5.25 and abs(position[2]-center[2]) <= 5.25
def perform_approach(guard, keyboard, mouse, observe, geometry, calibration, report, *, clock, sleep, notify, key_state_busy):
"""Run one bounded route through explicit adapters; retain partial failure evidence."""
require('approach_proof' not in report, 'Approach attempt already exists')
pinned = copy.deepcopy(geometry)
target, center = target_for(pinned)
require(calibration.get('completed') is True and calibration.get('nonce') == pinned['nonce'] and calibration.get('pixels') == 24, 'Missing accepted same-run look calibration')
gain = delta(yaw(calibration['response_direction']), yaw(calibration['baseline_direction']))/24
require(.01 <= abs(gain) <= .5, 'Measured calibration gain outside bounded proposal range')
ready_at = number(calibration['ready_monotonic']); ready_real = number(calibration['ready_real_ms'])
native_start = number(calibration['observer_start_real_ms'])
require(0 <= native_start <= ready_real and 2000 <= ready_real-native_start < 45000, 'Invalid original native observation window')
require(0 <= ready_at <= clock(), 'Invalid original READY monotonic time')
deadline = ready_at+35
require(clock() < deadline, 'Insufficient original approach time')
state = {'requested': True, 'completed': False, 'nonce': pinned['nonce'], 'geometry': pinned,
'target': list(target), 'deadline_monotonic': deadline, 'started_monotonic': clock(),
'finished_monotonic': None, 'motions': [], 'events': [], 'stops': [],
'samples': [], 'cumulative_horizontal_m': 0., 'unconfirmed_held_key': None,
'paired_release_errors': []}
report['approach_proof'] = state
previous = None; held = False; turn_count = 0; pixels = 0; pulse_count = 0
largest_stop = None; active_turn = None
def consume(s):
nonlocal previous
s = copy.deepcopy(s)
require(type(s['sample']) is int and 1 <= s['sample'] <= 450, 'Invalid native sample index')
real = number(s['real_ms']); received = number(s['received_monotonic'])
require(0 <= received <= clock(), 'Native receipt timestamp is invalid')
require(ready_real <= real and real-native_start < 45000 and real-ready_real < 35000, 'Original native lifetime exceeded')
require(s['geometry'] == pinned and s['phase'] == 1 and s['inventory'] is False, 'Original gate, prison phase or UI changed')
require(number(s['door_state']) == 0 and number(s['door_control']) == 0, 'Original gate is not closed and untouched')
for k in ('origin', 'direction', 'body_forward', 'input', 'velocity'):
s[k] = vector(s[k])
s['forward'] = number(s['forward']); yaw(s['direction']); yaw(s['body_forward'])
require(inside(s['origin'], center) and abs(s['origin'][1]-center[1]) <= 3, 'Body left bounded prison interior')
require(length(s['velocity']) <= 5, 'Native movement exceeds approach envelope')
require(abs(s['input'][0]) <= .01 and abs(s['input'][1]) <= .01 and -.01 <= s['input'][2] <= 1.01 and -.01 <= s['forward'] <= 1.01, 'Unexpected native movement input')
if active_turn is not None:
turn_yaw, turn_origin, turn_sign = active_turn
change = delta(yaw(s['direction']), turn_yaw)
require(abs(change) <= 15, 'Relative turn exceeded native angle bound')
require(horizontal(s['origin'], turn_origin) <= .05, 'Body translated during stationary heading')
if abs(change) >= .05:
require(change*turn_sign > 0, 'Native heading turned opposite to proposal')
if previous:
require(s['sample'] >= previous['sample'] and real >= previous['real_ms'], 'Native approach samples reversed')
if s['sample'] == previous['sample']:
require({k:v for k,v in s.items() if k != 'received_monotonic'} == {k:v for k,v in previous.items() if k != 'received_monotonic'}, 'Native sample changed without advancing')
else:
dt = real-previous['real_ms']
require(s['sample'] == previous['sample']+1 and 0 < dt <= 500, 'Native approach sampling gap')
distance = horizontal(s['origin'], previous['origin'])
require(distance <= .25+5*dt/1000, 'Native displacement exceeds continuous motion bound')
state['cumulative_horizontal_m'] += distance
require(state['cumulative_horizontal_m'] <= 6, 'Approach path budget exceeded')
if previous is None or s['sample'] != previous['sample']:
state['samples'].append(copy.deepcopy(s))
previous = s
return s
def read():
require(clock() < deadline, 'Original approach deadline exceeded')
guard.check()
batch = observe()
require(type(batch) is list and 1 <= len(batch) <= 450, 'Missing bounded native snapshot batch')
for item in batch:
s = consume(item)
require(clock() < deadline, 'Original approach deadline exceeded during observation')
require(0 <= clock()-s['received_monotonic'] <= .5, 'Native approach sample stale')
return s
def neutral(s):
return abs(s['forward']) <= .01 and length(s['input']) <= .01 and length(s['velocity']) <= .05
def wait_stopped(after, seconds, angle=None, moved_from=None, turn_sign=None):
end = min(deadline, clock()+seconds); first = None
while clock() < end:
s = read()
moved = moved_from is None or horizontal(s['origin'], moved_from) >= .05
if angle is not None:
change = delta(yaw(s['direction']), angle)
require(abs(change) <= 15, 'Relative turn exceeded native angle bound')
if abs(change) >= .05:
require(change*turn_sign > 0, 'Native heading turned opposite to proposal')
moved = moved and abs(change) >= .05
if s['sample'] > after and neutral(s) and moved:
if first is None:
first = s
elif s['real_ms']-first['real_ms'] >= 100:
if abs(delta(yaw(s['direction']), yaw(first['direction']))) <= .2 and horizontal(s['origin'], first['origin']) <= .02:
if angle is not None:
require(abs(delta(yaw(s['direction']), angle)) <= 15, 'Relative turn exceeded native angle bound')
require(clock() <= end, 'Settling exceeded original stage deadline')
return s
first = s
else:
first = None
sleep(.025)
raise Refused('Native full-stop or turn-settling acknowledgement missing')
def heading(s):
nonlocal turn_count, pixels, active_turn
end = min(deadline, clock()+20); growing = 0; flips = 0; old_sign = None
while True:
require(neutral(s), 'Heading correction requires stationary body')
wanted = math.degrees(math.atan2(target[0]-s['origin'][0], target[2]-s['origin'][2]))
error = delta(wanted, yaw(s['direction']))
if abs(error) <= 3:
return s
require(clock() < end and turn_count < 16, 'Heading request budget exceeded')
desired = max(-10., min(10., .75*error))
dx = int(round(desired/gain)); dx = max(-128, min(128, dx))
if dx == 0: dx = 1 if desired/gain > 0 else -1
require(pixels+abs(dx) <= 2048, 'Relative pixel budget exceeded')
before = read(); require(neutral(before), 'Body moved before turn')
keyboard.require_idle()
active_turn = (yaw(before['direction']), before['origin'], dx*gain)
mouse.move(dx, 0, guard)
turn_count += 1; pixels += abs(dx)
state['motions'].append({'dx': dx, 'dy': 0, 'monotonic': clock(), 'after_sample': before['sample']})
after = wait_stopped(before['sample'], 1.5, yaw(before['direction']), turn_sign=dx*gain)
state['motions'][-1]['response_sample'] = after['sample']
active_turn = None
require(horizontal(after['origin'], before['origin']) <= .05, 'Body translated during stationary heading')
change = delta(yaw(after['direction']), yaw(before['direction']))
require(abs(change) >= .05 and change*dx*gain > 0, 'Native heading failed or turned opposite to proposal')
new_error = delta(wanted, yaw(after['direction']))
growing = growing+1 if abs(new_error) >= abs(error) else 0
sign = new_error > 0
if old_sign is not None and sign != old_sign: flips += 1
old_sign = sign
require(growing < 2 and flips <= 2, 'Native heading failed to converge')
s = after
def release():
nonlocal held
# A native context failure does not prevent an owned release; only the
# full live private display/process/focus proof authorizes that release.
guard.check(); keyboard.send('W', False)
state['events'].append({'key': 'W', 'down': False, 'monotonic': clock()})
end = clock()+.35
while True:
guard.check()
try:
keyboard.require_idle()
break
except ValueError as error:
# Only the injected existing busy-state subtype is retryable.
if not isinstance(error, key_state_busy) or clock() >= end:
raise
sleep(min(.025, end-clock()))
held = False; state['unconfirmed_held_key'] = None
notify({'kind': 'release-confirmed', 'key': 'W'})
notify({'kind': 'approach-pending', 'deadline_monotonic': deadline})
try:
keyboard.require_idle()
current = read(); current = wait_stopped(current['sample']-1, 1.5)
initial_distance = horizontal(current['origin'], target)
require(.75 < initial_distance <= 6, 'Initial approach distance outside route scope')
while horizontal(current['origin'], target) > .75:
current = heading(current)
require(pulse_count < 12, 'Forward pulse budget exceeded')
remaining = horizontal(current['origin'], target)
reserve = (largest_stop if largest_stop is not None else 1.25)+.25
require(remaining > reserve, 'Insufficient distance for measured stopping reserve')
ang = math.radians(yaw(current['direction']))
projected = (current['origin'][0]+reserve*math.sin(ang), current['origin'][1], current['origin'][2]+reserve*math.cos(ang))
require(inside(projected, center), 'Stopping reserve leaves safe interior')
before = read(); require(neutral(before), 'Forward pulse requires full native stop')
remaining = horizontal(before['origin'], target)
wanted = math.degrees(math.atan2(target[0]-before['origin'][0], target[2]-before['origin'][2]))
require(abs(delta(wanted, yaw(before['direction']))) <= 3, 'Fresh heading changed before W')
require(remaining > reserve, 'Fresh position exhausted stopping reserve')
ang = math.radians(yaw(before['direction']))
projected = (before['origin'][0]+reserve*math.sin(ang), before['origin'][1], before['origin'][2]+reserve*math.cos(ang))
require(inside(projected, center), 'Fresh stopping reserve leaves safe interior')
keyboard.require_idle()
require(clock() < deadline, 'Original approach deadline exceeded before press')
held = True; state['unconfirmed_held_key'] = 'W'
notify({'kind': 'press-pending', 'key': 'W'})
keyboard.send('W', True); pressed = clock()
state['events'].append({'key': 'W', 'down': True, 'monotonic': pressed})
sleep(.05)
release()
released = state['events'][-1]['monotonic']
require(released-pressed <= .4, 'Actual W hold exceeded 400ms approach bound')
pulse_count += 1
after = wait_stopped(before['sample'], 1.5, moved_from=before['origin'])
require(any(s['forward'] > .01 and s['input'][2] > .01 for s in state['samples'] if before['sample'] < s['sample'] <= after['sample']), 'Native W acknowledgement missing from pulse samples')
distance = horizontal(before['origin'], after['origin'])
progress = remaining-horizontal(after['origin'], target)
require(.05 <= distance <= 1.25 and progress >= .05, 'Forward motion missed, overshot or moved away from target')
require(abs(delta(yaw(after['direction']), yaw(before['direction']))) <= 5, 'Heading drifted during forward pulse')
largest_stop = max(largest_stop or 0, distance)
state['stops'].append({'before_sample': before['sample'], 'after_sample': after['sample'], 'distance': distance, 'progress': progress})
current = after
final = read()
require(neutral(final) and horizontal(final['origin'], target) <= .75, 'Final native body is not stopped at approach target')
state['final_sample'] = final['sample']
keyboard.require_idle()
require(clock() < deadline, 'Original approach deadline exceeded at completion')
state['completed'] = True
return state
except Exception as error:
state['error'] = str(error)
raise
finally:
if held:
try: release()
except Exception as error: state['paired_release_errors'].append(str(error))
state['finished_monotonic'] = clock()
notify({'kind': 'approach-finished', 'successful': state['completed']})