From ec380c1e5982ef4c7d78a52e46ba99a6ddbbe4ce Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:16:47 -0700 Subject: [PATCH] Add fail-closed live supervisor policy and Galaxy controller boundary --- roadscore/prototype/live_controller.py | 47 ++++++++ roadscore/prototype/live_supervisor.py | 105 ++++++++++++++++++ roadscore/prototype/live_supervisor_README.md | 57 ++++++++++ roadscore/prototype/test_live_supervisor.py | 54 +++++++++ 4 files changed, 263 insertions(+) create mode 100644 roadscore/prototype/live_controller.py create mode 100644 roadscore/prototype/live_supervisor.py create mode 100644 roadscore/prototype/live_supervisor_README.md create mode 100644 roadscore/prototype/test_live_supervisor.py diff --git a/roadscore/prototype/live_controller.py b/roadscore/prototype/live_controller.py new file mode 100644 index 0000000000..8020a07150 --- /dev/null +++ b/roadscore/prototype/live_controller.py @@ -0,0 +1,47 @@ +"""Galaxy contract. Missing target health/lifecycle adapter is explicitly unavailable. + +No SSH, device probing, or process start occurs while importing/constructing. +The target adapter must retain supervisor ownership across separate UI requests. +""" +from pathlib import Path +import time +from live_supervisor import blocked_reason + + +class LiveController: + def __init__(self, root=Path('/data/roadscore'), *, adapter=None, clock=time.monotonic): + self.root=Path(root);self.clock=clock;self.adapter=adapter + if self.adapter is None: + try: + from live_target_adapter import create + except ModuleNotFoundError as error: + if error.name!='live_target_adapter':raise + else:self.adapter=create(self.root) + + def status(self): + if self.adapter is None: + return dict(available=False,enabled=False,state='COLD',can_enable=False, + reason='Live target health/lifecycle adapter is not installed; hardware readiness is unverified') + try: + status=self.adapter.status() + observation,authorization=self.adapter.health_and_authorization() + reason=blocked_reason(observation,authorization,self.clock(),require_parked=True) + return {**status,'available':True,'can_enable':not status.get('enabled',False) and not reason, + 'reason':reason or status.get('reason','')} + except Exception as error: + return dict(available=False,enabled=False,state='DEGRADED',can_enable=False,reason='Live supervisor status unavailable: '+str(error)) + + def set_enabled(self, enabled): + if type(enabled) is not bool:raise ValueError('enabled must be a boolean') + if self.adapter is None: + if not enabled:return self.status() + raise RuntimeError(self.status()['reason']) + if not enabled: + self.adapter.stop_owned() + return self.status() + observation,authorization=self.adapter.health_and_authorization() + reason=blocked_reason(observation,authorization,self.clock(),require_parked=True) + if reason:raise RuntimeError(reason) + # Adapter must recheck atomically with launch and continue its watchdog. + self.adapter.enable(observation,authorization) + return self.status() diff --git a/roadscore/prototype/live_supervisor.py b/roadscore/prototype/live_supervisor.py new file mode 100644 index 0000000000..2cc320ef7b --- /dev/null +++ b/roadscore/prototype/live_supervisor.py @@ -0,0 +1,105 @@ +"""Fail-closed live lifecycle policy. No Params writes, replay, or bench power code. + +A target integration must supply genuine current observations and own the child +processes. Importing this module never probes a device or starts a process. +""" +from dataclasses import dataclass +import math +import time +import uuid + + +@dataclass(frozen=True) +class Observation: + monotonic: float + parked: bool + model_fresh: bool + car_fresh: bool + modeld_healthy: bool + device_healthy: bool + chestnut_healthy: bool + baseline_id: str + car_id: str + driving_model_local: bool = False + + +@dataclass(frozen=True) +class Authorization: + baseline_id: str + car_id: str + coexistence_verified: bool = False + user_authorized: bool = False + + +def blocked_reason(observation, authorization, now, *, require_parked): + if not isinstance(observation, Observation) or not isinstance(authorization, Authorization): + return 'Target health and explicit car/baseline authorization are unavailable' + if authorization.user_authorized is not True or authorization.coexistence_verified is not True: + return 'Car baseline and ACE/modeld coexistence must be verified and authorized' + if not authorization.baseline_id or not authorization.car_id or (observation.baseline_id, observation.car_id) != (authorization.baseline_id, authorization.car_id): + return 'Current car or software baseline differs from the authorized configuration' + if not math.isfinite(observation.monotonic) or not 0 <= now-observation.monotonic <= 1.: + return 'Live health observation is stale or has an invalid clock' + if not all(value is True for value in (observation.model_fresh, observation.car_fresh, observation.modeld_healthy, + observation.device_healthy, observation.chestnut_healthy, observation.driving_model_local)): + return 'Fresh car/model input and healthy local driving model, device, and Chestnut are required' + if require_parked and observation.parked is not True: + return 'Park before preparing or enabling live RoadScore' + return '' + + +class LiveSupervisor: + """Adapter callbacks operate ONLY this session's worker/app, with no shell takeover. + + prepare(session_id) launches direct ACE with a fresh seed/current bank/quality. + start_app(session_id) starts app --input live using accepted initial music. + stop_owned() must terminate only this supervisor's owned process groups. + These callbacks are deliberately injected, never an implicit hardware adapter. + """ + def __init__(self, prepare, start_app, stop_owned, *, clock=time.monotonic): + self.prepare=prepare;self.start_app=start_app;self.stop_owned=stop_owned;self.clock=clock + self.state='COLD';self.reason='';self.session_id=None;self.enabled=False;self.driver_ready=False + + def status(self): + return dict(available=True,enabled=self.enabled,state=self.state,reason=self.reason, + session_id=self.session_id,driver_ready=self.driver_ready) + + def enable(self, observation, authorization): + if self.enabled:return self.status() + reason=blocked_reason(observation,authorization,self.clock(),require_parked=True) + if reason:raise RuntimeError(reason) + self.session_id=uuid.uuid4().hex;self.driver_ready=False;self.enabled=True;self.state='PREPARING';self.reason='Preparing accepted audio while parked' + try:self.prepare(self.session_id) + except Exception: + self.stop('Preparation failed');raise + return self.status() + + def confirm_driver_ready(self, session_id, observation, authorization): + if not self.enabled or self.state!='READY' or session_id!=self.session_id: + raise RuntimeError('Driver readiness must confirm the current prepared live session') + reason=blocked_reason(observation,authorization,self.clock(),require_parked=True) + if reason:raise RuntimeError(reason) + try:self.start_app(self.session_id) + except Exception: + self.stop('Live input/audio startup failed');raise + self.driver_ready=True;self.state='STARTING';self.reason='Waiting for live input/audio readiness' + return self.status() + + def tick(self, observation, authorization, *, worker_healthy, accepted_ready=False, app_ready=False, app_healthy=True): + if not self.enabled:return self.status() + reason=blocked_reason(observation,authorization,self.clock(),require_parked=not self.driver_ready) + if reason or not worker_healthy or (self.driver_ready and not app_healthy): + return self.stop(reason or 'Owned composer or audio process failed') + if self.state=='PREPARING' and accepted_ready: + self.state='READY';self.reason='Prepared while parked; explicit driver-ready confirmation required' + if self.state=='STARTING' and app_ready: + self.state='LIVE';self.reason='' + return self.status() + + def stop(self, reason='Stopped by operator'): + # Always callable, including when health is stale or car is moving. + self.enabled=False;self.driver_ready=False;self.state='STOPPING';self.reason=reason + try:self.stop_owned() + except Exception: + self.state='DEGRADED';self.reason='Owned process shutdown failed; inspect target supervisor';raise + self.state='COLD';return self.status() diff --git a/roadscore/prototype/live_supervisor_README.md b/roadscore/prototype/live_supervisor_README.md new file mode 100644 index 0000000000..4e168092d4 --- /dev/null +++ b/roadscore/prototype/live_supervisor_README.md @@ -0,0 +1,57 @@ +# Live supervisor candidate: unavailable until target adapter validation + +`live_controller.LiveController(root=Path)` provides the Galaxy contract: +`status()` returns available/enabled/state/can_enable/reason/session_id, and +`set_enabled(bool)` starts parked preparation or stops only owned live processes. +OFF bypasses health/parked gates. ON rechecks current health server-side. + +**There is no installed target adapter in this change.** Default status is +available=false, can_enable=false. No process launches, hardware probes, Params +writes, car-control publishers, model selection, or device access occur here. +This is reviewed orchestration policy plus an explicit disabled integration +boundary, not a road-tested live service. + +The controller expects `live_target_adapter.create(root)` to return a persistent +service client with status(), health_and_authorization(), enable(observation, +authorization), and stop_owned(). Its daemon can use LiveSupervisor. It must +atomically revalidate on start and tick frequently enough to meet the health +freshness bound. Fresh accepted worker readiness must match the exact session +seed/bank/profile; an old worker_ready file is not sufficient. + +The adapter must collect genuine modelV2/carState/manager/device/Chestnut health. +Observation age must be <=1 second. Input booleans must reflect per-message ages, +model frameDropPerc/modelExecutionTime and process health under an owner-validated +budget; a recent collector timestamp must not make stale source messages fresh. +Parkedness comes from fresh standstill/speed/gear evidence, **not IsOnroad=false**; +an ignition-on parked car is permitted. Verify actual driving-model placement +is local from current modeld/Chestnut telemetry, including UsbGpu* Params and +chestnutState as appropriate, without writing them. User's requested small model +alone does not prove local execution. Missing/unknown placement fails closed. +Persist explicit user authorization and verified coexistence bound to current +car and code/config baseline. No such verification is manufactured here. + +Sequence: authorized healthy parked environment -> PREPARING -> accepted READY +-> explicit driver-ready confirmation of the exact session while parked -> +STARTING -> verified app/audio readiness -> LIVE. Movement before driver-ready, +stale health, modeld degradation, or worker/app failure invokes owned-only stop. +No auto-resume after failure. Driver-ready is not inferred from enable or from +vehicle movement. No navigation is required; app --input live must use fresh +current messages only and preserve the no-future-input policy. + +ACE worker can run directly with current cached conditioning, fresh session seed, +quality policy, and GPU/session locks. `run_worker.sh` supplies interpreter/device +and conservative thread-count environment. It does not mutate CPU settings. +**Do not use power_worker.py or worker_service.py for this lane**: both remain +intentionally offroad/bench guarded; power_worker also changes CPU online bits, +clock/governor and affinity. Do not weaken those guards, stop manager/modeld, set +replay namespaces, publish fake carState, or reset another worker. Target adapter +must refuse another GPU/session owner and retain only its own process-group +handles for stop. Preserve active results/current and create unique session +archives; output/Bluetooth setup requires root lifecycle integration. + +Actual ACE/modeld CPU/memory/thermal coexistence has not been measured. Chestnut +model allocation, USB link faults and normal CPU scheduling can still affect the +platform. Offroad resident results do not establish driving coexistence. The +candidate stays unavailable until the sole hardware owner validates the read-only +collector and coexistence baseline; explicit driver-ready is separately required +before an attended road test. diff --git a/roadscore/prototype/test_live_supervisor.py b/roadscore/prototype/test_live_supervisor.py new file mode 100644 index 0000000000..502eeb9a55 --- /dev/null +++ b/roadscore/prototype/test_live_supervisor.py @@ -0,0 +1,54 @@ +from dataclasses import replace +from unittest.mock import Mock +import pytest +from live_supervisor import Observation,Authorization,LiveSupervisor,blocked_reason +from live_controller import LiveController + +GOOD=Observation(10.,True,True,True,True,True,True,'baseline','car',True) +AUTH=Authorization('baseline','car',True,True) + +def make(): + calls=Mock() + return LiveSupervisor(calls.prepare,calls.start_app,calls.stop,clock=lambda:10.),calls + +@pytest.mark.parametrize('field,value',[('monotonic',8.),('monotonic',11.),('monotonic',float('nan')),('parked',False),('driving_model_local',False),('model_fresh',False),('car_fresh',False),('modeld_healthy',False),('device_healthy',False),('chestnut_healthy',False),('baseline_id','different'),('car_id','different')]) +def test_fail_closed_before_process_start(field,value): + sup,calls=make() + with pytest.raises(RuntimeError):sup.enable(replace(GOOD,**{field:value}),AUTH) + calls.prepare.assert_not_called() + +def test_capability_not_implied_by_car_connection(): + sup,calls=make() + with pytest.raises(RuntimeError):sup.enable(GOOD,replace(AUTH,coexistence_verified=False)) + calls.prepare.assert_not_called() + +def test_gate_then_health_failure_stops_only_owned_callbacks(): + sup,calls=make();sup.enable(GOOD,AUTH) + sup.tick(GOOD,AUTH,worker_healthy=True,accepted_ready=True) + calls.start_app.assert_not_called() + with pytest.raises(RuntimeError):sup.confirm_driver_ready('previous-session',GOOD,AUTH) + sup.confirm_driver_ready(sup.session_id,GOOD,AUTH) + assert sup.state=='STARTING' + sup.tick(replace(GOOD,parked=False),AUTH,worker_healthy=True,app_ready=True) + assert sup.state=='LIVE' + sup.tick(replace(GOOD,model_fresh=False),AUTH,worker_healthy=True) + calls.stop.assert_called_once();assert sup.state=='COLD' + +def test_movement_before_ready_stops_preparation(): + sup,calls=make();sup.enable(GOOD,AUTH) + sup.tick(replace(GOOD,parked=False),AUTH,worker_healthy=True) + calls.stop.assert_called_once();calls.start_app.assert_not_called() + +def test_controller_off_ignores_missing_health_and_only_stops_adapter(): + adapter=Mock();adapter.status.return_value={'enabled':False,'state':'COLD'} + adapter.health_and_authorization.side_effect=RuntimeError('stale') + controller=LiveController(adapter=adapter,clock=lambda:10.) + controller.set_enabled(False);adapter.stop_owned.assert_called_once() + with pytest.raises(RuntimeError):controller.set_enabled(True) + adapter.enable.assert_not_called() + +def test_missing_adapter_unavailable(): + controller=LiveController() + assert not controller.status()['available'] and not controller.status()['can_enable'] + with pytest.raises(RuntimeError):controller.set_enabled(True) + assert controller.set_enabled(False)['enabled'] is False