mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-04 13:24:13 +08:00
Add fail-closed live supervisor policy and Galaxy controller boundary
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user