| # Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Public phase 1 host API: plan-driven runs composing consent, the | |
| transport, and evidence. | |
| The workflow is prepare (validate the plan, resolve consent -- no target | |
| access), execute (open a session with the exact plan-derived allowlist, | |
| run bounded operations, drain audit), and finalize (hash evidence, | |
| manifest last). Missing consent fails closed, and no denial or failure | |
| skips evidence finalization. | |
| """ | |
| import asyncio | |
| import dataclasses | |
| from collections.abc import Awaitable, Callable, Mapping, Sequence | |
| from pathlib import Path | |
| from typing import Any, Literal | |
| from driver_lab.consent import ( | |
| READ_WARNING, | |
| WRITE_WARNING, | |
| ConsentDecision, | |
| ConsentPrompt, | |
| grant_from_decision, | |
| ) | |
| from driver_lab.discovery import ( | |
| DEFAULT_PROXY_DRIVER_URL, | |
| DiscoveryError, | |
| NodeDescription, | |
| NodeDiscovery, | |
| NodeSummary, | |
| ProxyActivator, | |
| ) | |
| from driver_lab.evidence import EvidenceRecorder | |
| from driver_lab.models import AccessClass, AccessRequest, ResourceKind | |
| from driver_lab.permissions import ( | |
| GrantStoreError, | |
| Outcome, | |
| Resolution, | |
| add_grant, | |
| load_grants, | |
| resolve, | |
| ) | |
| from driver_lab.plans import is_mutating_plan, plan_digest, validate_plan | |
| from driver_lab.recovery import ( | |
| RecoveryController, | |
| SerialCapture, | |
| SerialCaptureResult, | |
| SerialCaptureSession, | |
| detect_panic, | |
| extract_panic_summary, | |
| ) | |
| from driver_lab.session import ( | |
| AccessRequirements, | |
| HardwareSession, | |
| SessionCapabilities, | |
| ) | |
| from driver_lab.transport import ( | |
| STALE_REJECTIONS, | |
| AllowRule, | |
| DirectTransport, | |
| Expectations, | |
| OpenSessionRejected, | |
| OperationDenied, | |
| ProxyDescription, | |
| ProxyTransport, | |
| ResourceInfo, | |
| SequenceItem, | |
| SessionContext, | |
| SessionMode, | |
| SnapshotItem, | |
| TransportError, | |
| ) | |
| EXIT_SUCCESS = 0 | |
| EXIT_PERMISSION = 2 | |
| EXIT_STALE = 3 | |
| EXIT_OPERATION = 4 | |
| EXIT_TRANSPORT = 5 | |
| EXIT_EVIDENCE = 7 | |
| EXIT_ACTIVATION = 8 | |
| EXIT_UNSUPPORTED = 10 | |
| # What a phase 1 proxy-mode run guarantees. Requirements a plan declares | |
| # are checked against this before any target connection, so an | |
| # unsupported guarantee never silently downgrades. | |
| PROXY_CAPABILITIES = { | |
| "mode": "proxy", | |
| "target_policy": True, | |
| "target_audit": True, | |
| "target_local_timing": False, | |
| } | |
| # Direct mode connects to a published protocol without proxy mediation. | |
| # It makes no claim to private MMIO, target policy, or target audit. | |
| DIRECT_CAPABILITIES = { | |
| "mode": "direct", | |
| "target_policy": False, | |
| "target_audit": False, | |
| "target_local_timing": False, | |
| } | |
| # Phase 2 in-situ mode connects directly to a live driver's embedded | |
| # fuchsia.driver.lab.Service endpoint without unbinding the driver. | |
| IN_SITU_CAPABILITIES = { | |
| "mode": "in-situ", | |
| "target_policy": True, | |
| "target_audit": True, | |
| "target_local_timing": True, | |
| } | |
| _DEFERRED_ARTIFACTS = ( | |
| "target.description.json", | |
| "permission-resolution.json", | |
| "operations.jsonl", | |
| "target-audit.jsonl", | |
| ) | |
| # Spec-defined artifacts a phase 1 read-only run can never produce. | |
| # Marked not applicable rather than silently omitted: takeover | |
| # restoration and proxy activation are phase 2; mutation approval, | |
| # node discovery snapshots, serial capture, and the interpretation | |
| # layer have not landed yet. | |
| _NOT_APPLICABLE_ARTIFACTS = ( | |
| "approval.json", | |
| "node.before.json", | |
| "node.after.json", | |
| "proxy-access.jsonl", | |
| "restoration.json", | |
| "serial.log", | |
| "host-events.jsonl", | |
| "interpretation.json", | |
| ) | |
| class DriverLabError(Exception): | |
| """A host-side configuration or usage error.""" | |
| @dataclasses.dataclass(frozen=True) | |
| class ReadRecord: | |
| """One completed 32-bit read from a plan run.""" | |
| operation_index: int | |
| resource: str | |
| offset: int | |
| value: int | |
| audit_seq: int | |
| timestamp_ns: int | |
| @dataclasses.dataclass(frozen=True) | |
| class WriteRecord: | |
| """One completed 32-bit write from a plan run.""" | |
| operation_index: int | |
| resource: str | |
| offset: int | |
| value: int | |
| write_mask: int | |
| readback_value: int | None | |
| audit_seq: int | |
| timestamp_ns: int | |
| @dataclasses.dataclass(frozen=True) | |
| class PollRecord: | |
| """One completed 32-bit poll from a plan run.""" | |
| operation_index: int | |
| resource: str | |
| offset: int | |
| expected: int | |
| mask: int | |
| value: int | |
| audit_seq: int | |
| timestamp_ns: int | |
| @dataclasses.dataclass(frozen=True) | |
| class SequenceItemRecord: | |
| """Outcome of one item executed in a sequence.""" | |
| index: int | |
| ok: bool | |
| kind: str | |
| value: int | None = None | |
| readback_value: int | None = None | |
| audit_seq: int = 0 | |
| timestamp_ns: int = 0 | |
| error: str | None = None | |
| @dataclasses.dataclass(frozen=True) | |
| class SequenceRecord: | |
| """One executed sequence from a plan run.""" | |
| operation_index: int | |
| results: tuple[SequenceItemRecord, ...] | |
| complete: bool | |
| @dataclasses.dataclass(frozen=True) | |
| class RunResult: | |
| """The outcome of one plan run. Evidence is always finalized.""" | |
| exit_category: int | |
| evidence_dir: Path | |
| plan_digest: str | |
| reads: tuple[ReadRecord, ...] = () | |
| writes: tuple[WriteRecord, ...] = () | |
| polls: tuple[PollRecord, ...] = () | |
| sequences: tuple[SequenceRecord, ...] = () | |
| calls: tuple[Mapping[str, Any], ...] = () | |
| failure: str | None = None | |
| @property | |
| def ok(self) -> bool: | |
| """Whether the run succeeded completely.""" | |
| return self.exit_category == EXIT_SUCCESS | |
| def interpret(self) -> dict[str, Any]: | |
| """Generates evidence-linked interpretation report per Spec Section 17.""" | |
| from driver_lab.recovery import interpret_evidence | |
| return interpret_evidence(self.evidence_dir) | |
| @dataclasses.dataclass(frozen=True) | |
| class _ReadOp: | |
| index: int | |
| resource: ResourceInfo | |
| offset: int | |
| kind: str = "mmio_read32" | |
| @dataclasses.dataclass(frozen=True) | |
| class _SnapshotOp: | |
| index: int | |
| items: tuple[tuple[ResourceInfo, int], ...] | |
| @dataclasses.dataclass(frozen=True) | |
| class _WriteOp: | |
| index: int | |
| resource: ResourceInfo | |
| offset: int | |
| value: int | |
| write_mask: int | |
| precondition: tuple[int, int] | None | |
| readback: bool | |
| kind: str = "mmio_write32" | |
| @dataclasses.dataclass(frozen=True) | |
| class _PollOp: | |
| index: int | |
| resource: ResourceInfo | |
| offset: int | |
| expected: int | |
| mask: int | |
| interval_ns: int | |
| timeout_ns: int | |
| kind: str = "mmio_poll32" | |
| @dataclasses.dataclass(frozen=True) | |
| class _SequenceOp: | |
| index: int | |
| items: tuple[SequenceItem, ...] | |
| has_mutation: bool | |
| class _StaleResource(Exception): | |
| pass | |
| class DriverLab: | |
| """Facade over one target for plan-driven runs.""" | |
| def __init__( | |
| self, | |
| transport: ProxyTransport | DirectTransport, | |
| *, | |
| grants_path: Path, | |
| evidence_root: Path, | |
| target_scope: str, | |
| node_id: str, | |
| consent: ConsentPrompt | None = None, | |
| discovery: NodeDiscovery | None = None, | |
| activator: ProxyActivator | None = None, | |
| serial_capture: SerialCapture | None = None, | |
| recovery: RecoveryController | None = None, | |
| driver_moniker: str | None = None, | |
| driver_url: str | None = None, | |
| ) -> None: | |
| self._transport = transport | |
| self._grants_path = grants_path | |
| self._evidence_root = evidence_root | |
| self._target_scope = target_scope | |
| self._node_id = node_id | |
| self._consent = consent | |
| self._discovery = discovery | |
| self._activator = activator | |
| self._serial_capture = serial_capture | |
| self._recovery = recovery | |
| self._driver_moniker = driver_moniker | |
| self._driver_url = driver_url | |
| @property | |
| def serial_capture(self) -> SerialCapture | None: | |
| return self._serial_capture | |
| @property | |
| def recovery(self) -> RecoveryController | None: | |
| return self._recovery | |
| @property | |
| def driver_moniker(self) -> str | None: | |
| return self._driver_moniker | |
| @property | |
| def driver_url(self) -> str | None: | |
| return self._driver_url | |
| async def recover_target(self, mode: str = "normal") -> None: | |
| """Triggers out-of-band recovery. | |
| Per Spec Section 14.3: Recovery creates a new run context; it does | |
| not resume old mutations or retry failed operations. | |
| """ | |
| if self._recovery is None: | |
| raise DriverLabError( | |
| "no recovery controller configured for this DriverLab instance" | |
| ) | |
| await self._recovery.reboot(mode=mode) | |
| async def list_nodes( | |
| self, | |
| node_filter: list[str] | None = None, | |
| exact_match: bool = False, | |
| ) -> list[NodeSummary]: | |
| """Discovers nodes on the target control plane.""" | |
| if self._discovery is None: | |
| raise DriverLabError( | |
| "node discovery is not configured for this DriverLab instance" | |
| ) | |
| return await self._discovery.list_nodes(node_filter, exact_match) | |
| async def find_debug_capable_drivers(self) -> list[NodeSummary]: | |
| """Discovers active bound drivers exposing fuchsia.driver.lab.Service (Phase 2).""" | |
| if self._discovery is None: | |
| raise DriverLabError( | |
| "node discovery is not configured for this DriverLab instance" | |
| ) | |
| return await self._discovery.find_debug_capable() | |
| async def describe_node(self, node_id: str) -> NodeDescription | None: | |
| """Describes a node by moniker.""" | |
| if self._discovery is None: | |
| raise DriverLabError( | |
| "node discovery is not configured for this DriverLab instance" | |
| ) | |
| return await self._discovery.describe_node(node_id) | |
| async def describe_driver(self) -> ProxyDescription: | |
| """Fetches ProxyDescription directly from the live driver's embedded endpoint.""" | |
| if not isinstance(self._transport, ProxyTransport): | |
| raise DriverLabError( | |
| "Proxy/in-situ transport is not configured for this DriverLab instance" | |
| ) | |
| return await self._transport.describe() | |
| async def bind_proxy( | |
| self, | |
| node_id: str, | |
| driver_url: str = DEFAULT_PROXY_DRIVER_URL, | |
| ) -> None: | |
| """Activates the proxy driver on an unclaimed node.""" | |
| if self._activator is None: | |
| raise DriverLabError( | |
| "proxy activator is not configured for this DriverLab instance" | |
| ) | |
| await self._activator.bind_proxy(node_id, driver_url) | |
| async def end_proxy( | |
| self, | |
| node_id: str, | |
| driver_url: str = DEFAULT_PROXY_DRIVER_URL, | |
| ) -> None: | |
| """Ends proxy access and unbinds/disables the proxy driver.""" | |
| if self._activator is None: | |
| raise DriverLabError( | |
| "proxy activator is not configured for this DriverLab instance" | |
| ) | |
| await self._activator.end_proxy(node_id, driver_url) | |
| @classmethod | |
| async def connect( | |
| cls, | |
| target: str = "default", | |
| *, | |
| transport: str = "auto", | |
| timeout_s: float = 10.0, | |
| grants_path: Path | None = None, | |
| evidence_root: Path | None = None, | |
| target_scope: str | None = None, | |
| node_id: str | None = None, | |
| driver_moniker: str | None = None, | |
| driver_url: str | None = None, | |
| consent: ConsentPrompt | None = None, | |
| discovery: NodeDiscovery | None = None, | |
| activator: ProxyActivator | None = None, | |
| serial_capture: SerialCapture | None = None, | |
| recovery: RecoveryController | None = None, | |
| proxy_transport: ProxyTransport | None = None, | |
| direct_transport: DirectTransport | None = None, | |
| ) -> "DriverLab": | |
| """Connects to a target device or directly to a live driver moniker (Phase 2).""" | |
| endpoint_moniker = driver_moniker or node_id or target | |
| active_transport: ProxyTransport | DirectTransport | |
| if proxy_transport is not None: | |
| active_transport = proxy_transport | |
| elif direct_transport is not None: | |
| active_transport = direct_transport | |
| elif transport in ("proxy", "in-situ", "auto"): | |
| from driver_lab.fidl_transport import connect_transport | |
| active_transport = connect_transport( | |
| endpoint_moniker, target=target | |
| ) | |
| else: | |
| raise DriverLabError( | |
| "Direct transport must be explicitly provided via direct_transport" | |
| ) | |
| if discovery is None: | |
| try: | |
| from driver_lab.discovery import connect_discovery | |
| discovery = connect_discovery( | |
| target=None if target == "default" else target | |
| ) | |
| except Exception: | |
| discovery = None | |
| if activator is None: | |
| try: | |
| from driver_lab.discovery import connect_activator | |
| activator = connect_activator( | |
| target=None if target == "default" else target | |
| ) | |
| except Exception: | |
| activator = None | |
| return cls( | |
| transport=active_transport, | |
| grants_path=grants_path or Path("grants.toml"), | |
| evidence_root=evidence_root or Path("evidence"), | |
| target_scope=target_scope or target, | |
| node_id=node_id or driver_moniker or target, | |
| consent=consent, | |
| discovery=discovery, | |
| activator=activator, | |
| serial_capture=serial_capture, | |
| recovery=recovery, | |
| driver_moniker=driver_moniker, | |
| driver_url=driver_url, | |
| ) | |
| async def attach( | |
| self, | |
| node_id: str, | |
| *, | |
| mode: Literal["auto", "direct", "proxy", "in-situ"] = "auto", | |
| requirements: AccessRequirements | None = None, | |
| session_mode: SessionMode = SessionMode.READ_ONLY, | |
| allowlist: Sequence[AllowRule] | None = None, | |
| expectations: Expectations | Mapping[str, Any] | None = None, | |
| context: SessionContext | None = None, | |
| consent: ConsentPrompt | None = None, | |
| ) -> HardwareSession: | |
| """Attaches to a node or active driver moniker, returning a HardwareSession.""" | |
| if mode not in ("auto", "direct", "proxy", "in-situ"): | |
| raise ValueError( | |
| f"Invalid mode '{mode}': must be 'auto', 'direct', 'proxy', or 'in-situ'" | |
| ) | |
| is_in_situ = mode == "in-situ" or self._driver_moniker is not None | |
| if not is_in_situ and self._discovery is not None: | |
| try: | |
| node_desc = await self._discovery.describe_node(node_id) | |
| if ( | |
| node_desc is not None | |
| and not node_desc.is_unclaimed | |
| and node_desc.debug_capable | |
| ): | |
| is_in_situ = True | |
| except Exception: | |
| pass | |
| if mode == "auto": | |
| if is_in_situ: | |
| selected_mode = "in-situ" | |
| elif requirements and ( | |
| requirements.needs_mmio | |
| or requirements.needs_sequence | |
| or requirements.needs_target_timing | |
| or requirements.needs_target_policy | |
| or requirements.needs_target_audit | |
| or requirements.is_mutating | |
| ): | |
| selected_mode = "proxy" | |
| elif isinstance(self._transport, DirectTransport) or ( | |
| requirements and requirements.protocol | |
| ): | |
| selected_mode = "direct" | |
| else: | |
| selected_mode = "proxy" | |
| elif mode == "direct": | |
| if requirements and ( | |
| requirements.needs_mmio | |
| or requirements.needs_sequence | |
| or requirements.needs_target_timing | |
| or requirements.needs_target_policy | |
| or requirements.needs_target_audit | |
| or requirements.is_mutating | |
| ): | |
| raise DriverLabError( | |
| "Direct mode cannot satisfy requested requirements (requires proxy or in-situ mode)" | |
| ) | |
| selected_mode = "direct" | |
| elif mode == "in-situ": | |
| selected_mode = "in-situ" | |
| else: | |
| selected_mode = "proxy" | |
| if selected_mode == "direct": | |
| if not isinstance(self._transport, DirectTransport): | |
| raise DriverLabError( | |
| "Direct transport is not configured for this DriverLab instance" | |
| ) | |
| direct_desc = await self._transport.describe() | |
| direct_ctx = context or SessionContext( | |
| run_id=f"direct-{node_id}", | |
| case_id="interactive", | |
| plan_digest="direct-session", | |
| host_tool_version="0.1.0", | |
| ) | |
| direct_session = await self._transport.open_direct_session( | |
| direct_ctx | |
| ) | |
| caps = SessionCapabilities( | |
| mode="direct", | |
| target_policy=False, | |
| target_audit=False, | |
| target_local_timing=False, | |
| fault_isolation="driver_host", | |
| production_driver_active=bool(direct_desc.bound_driver_url), | |
| restoration_required=False, | |
| resources=(), | |
| protocols=(direct_desc.protocol_name,), | |
| ) | |
| return HardwareSession( | |
| capabilities=caps, | |
| direct_session=direct_session, | |
| direct_description=direct_desc, | |
| ) | |
| # Proxy or In-Situ mode | |
| if not isinstance(self._transport, ProxyTransport): | |
| raise DriverLabError( | |
| "Proxy transport is not configured for this DriverLab instance" | |
| ) | |
| proxy_desc = await self._transport.describe() | |
| if session_mode == SessionMode.MUTATING: | |
| active_consent = consent or self._consent | |
| if active_consent is None: | |
| raise DriverLabError( | |
| "Mutating session requires operator consent, but no consent prompt is configured" | |
| ) | |
| req = AccessRequest( | |
| target_scope=self._target_scope, | |
| node_id=node_id, | |
| resource_digest=proxy_desc.resource_digest, | |
| resource="*", | |
| offset=0, | |
| width=4, | |
| access=AccessClass.WRITE, | |
| ) | |
| decision = await active_consent.request_consent(req, WRITE_WARNING) | |
| if decision != ConsentDecision.ALLOW_ONCE: | |
| raise DriverLabError( | |
| f"Mutating session was not authorized by operator (decision: {decision})" | |
| ) | |
| rules: list[AllowRule] = [] | |
| if allowlist is not None: | |
| rules = list(allowlist) | |
| else: | |
| for res in proxy_desc.resources: | |
| if res.kind == ResourceKind.INTERRUPT: | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=0, | |
| width=0, | |
| access=AccessClass.INTERRUPT, | |
| ) | |
| ) | |
| continue | |
| if res.kind != ResourceKind.MMIO: | |
| width = 1 if res.kind == ResourceKind.GPIO else 32 | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=0, | |
| width=width, | |
| access=AccessClass.PROTOCOL, | |
| ) | |
| ) | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=0, | |
| width=width, | |
| access=AccessClass.SEQUENCE, | |
| ) | |
| ) | |
| continue | |
| mmio_resources = [ | |
| res | |
| for res in proxy_desc.resources | |
| if res.kind == ResourceKind.MMIO | |
| ] | |
| max_mmio_rules = 250 - len(rules) | |
| if mmio_resources and max_mmio_rules > 0: | |
| rules_per_res = max(10, max_mmio_rules // len(mmio_resources)) | |
| for res in mmio_resources: | |
| res_rules = 0 | |
| for off in range(0, min(res.logical_size, 256), 4): | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=off, | |
| width=4, | |
| access=AccessClass.READ_ONCE, | |
| ) | |
| ) | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=off, | |
| width=4, | |
| access=AccessClass.SNAPSHOT, | |
| ) | |
| ) | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=off, | |
| width=4, | |
| access=AccessClass.POLL, | |
| ) | |
| ) | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=off, | |
| width=4, | |
| access=AccessClass.SEQUENCE, | |
| ) | |
| ) | |
| res_rules += 4 | |
| if session_mode == SessionMode.MUTATING: | |
| rules.append( | |
| AllowRule( | |
| resource=res.id, | |
| offset=off, | |
| width=4, | |
| access=AccessClass.WRITE, | |
| ) | |
| ) | |
| res_rules += 1 | |
| if res_rules >= rules_per_res or len(rules) >= 250: | |
| break | |
| if len(rules) >= 250: | |
| break | |
| proxy_ctx = context or SessionContext( | |
| run_id=f"{selected_mode}-{node_id}", | |
| case_id="interactive", | |
| plan_digest=f"{selected_mode}-session", | |
| host_tool_version="0.1.0", | |
| ) | |
| exp: Expectations | |
| if expectations is not None: | |
| if isinstance(expectations, Expectations): | |
| exp = expectations | |
| else: | |
| exp = Expectations( | |
| boot_id=str(expectations["boot_id"]), | |
| proxy_generation=int(expectations["proxy_generation"]), | |
| resource_digest=str(expectations["resource_digest"]), | |
| policy_digest=str(expectations["policy_digest"]), | |
| ) | |
| else: | |
| exp = Expectations( | |
| boot_id=proxy_desc.boot_id, | |
| proxy_generation=proxy_desc.proxy_generation, | |
| resource_digest=proxy_desc.resource_digest, | |
| policy_digest=proxy_desc.policy_digest, | |
| ) | |
| try: | |
| proxy_session = await self._transport.open_session( | |
| context=proxy_ctx, | |
| mode=session_mode, | |
| expectations=exp, | |
| allowlist=rules, | |
| ) | |
| except OpenSessionRejected as exc: | |
| if allowlist is not None: | |
| raise | |
| # When no explicit allowlist was provided and the embedded driver's | |
| # target ceiling hard-denied or write-restricted some default probe | |
| # offsets, probe individual rules to retain only ceiling-permitted rules. | |
| permitted_rules: list[AllowRule] = [] | |
| for candidate in rules: | |
| try: | |
| probe_sess = await self._transport.open_session( | |
| context=proxy_ctx, | |
| mode=SessionMode.READ_ONLY | |
| if candidate.access != AccessClass.WRITE | |
| else session_mode, | |
| expectations=exp, | |
| allowlist=[candidate], | |
| ) | |
| await probe_sess.close() | |
| permitted_rules.append(candidate) | |
| except OpenSessionRejected: | |
| continue | |
| proxy_session = await self._transport.open_session( | |
| context=proxy_ctx, | |
| mode=session_mode, | |
| expectations=exp, | |
| allowlist=permitted_rules, | |
| ) | |
| caps = SessionCapabilities( | |
| mode=selected_mode, | |
| target_policy=True, | |
| target_audit=True, | |
| target_local_timing=True, | |
| fault_isolation="driver_host", | |
| production_driver_active=(selected_mode == "in-situ" or is_in_situ), | |
| restoration_required=False, | |
| resources=tuple(r.name for r in proxy_desc.resources), | |
| protocols=(), | |
| ) | |
| cursor = 0 | |
| async def _drain_audit() -> None: | |
| nonlocal cursor | |
| page = await proxy_session.read_audit(cursor=cursor, limit=64) | |
| cursor = page.next_cursor | |
| return HardwareSession( | |
| capabilities=caps, | |
| proxy_session=proxy_session, | |
| proxy_description=proxy_desc, | |
| audit_drainer=_drain_audit, | |
| ) | |
| def _derive( | |
| self, canonical: Mapping[str, Any], description: ProxyDescription | |
| ) -> tuple[ | |
| list[_ReadOp | _SnapshotOp | _WriteOp | _PollOp | _SequenceOp], | |
| list[tuple[int, AccessRequest]], | |
| ]: | |
| operations: list[ | |
| _ReadOp | _SnapshotOp | _WriteOp | _PollOp | _SequenceOp | |
| ] = [] | |
| requests: list[tuple[int, AccessRequest]] = [] | |
| def lookup(name: str) -> ResourceInfo: | |
| resource = description.resource_named(name) | |
| if resource is None: | |
| raise _StaleResource(f"resource not offered by target: {name}") | |
| return resource | |
| def request( | |
| resource: ResourceInfo, offset: int, access: AccessClass | |
| ) -> AccessRequest: | |
| return AccessRequest( | |
| target_scope=self._target_scope, | |
| node_id=self._node_id, | |
| resource_digest=resource.digest, | |
| resource=resource.name, | |
| offset=offset, | |
| width=4, | |
| access=access, | |
| ) | |
| for index, operation in enumerate(canonical["operations"]): | |
| kind = operation["kind"] | |
| if kind in ("mmio_read32", "state_read32"): | |
| resource = lookup(operation["resource"]) | |
| offset = operation["offset"] | |
| operations.append( | |
| _ReadOp( | |
| index=index, | |
| resource=resource, | |
| offset=offset, | |
| kind=kind, | |
| ) | |
| ) | |
| requests.append( | |
| (index, request(resource, offset, AccessClass.READ_ONCE)) | |
| ) | |
| elif kind == "mmio_snapshot32": | |
| items = tuple( | |
| (lookup(item["resource"]), item["offset"]) | |
| for item in operation["items"] | |
| ) | |
| operations.append(_SnapshotOp(index=index, items=items)) | |
| requests.extend( | |
| (index, request(resource, offset, AccessClass.SNAPSHOT)) | |
| for resource, offset in items | |
| ) | |
| elif kind in ("mmio_write32", "knob_write32", "trigger_write32"): | |
| resource = lookup(operation["resource"]) | |
| offset = operation["offset"] | |
| precondition = None | |
| if ( | |
| "precondition" in operation | |
| and operation["precondition"] is not None | |
| ): | |
| precondition = ( | |
| operation["precondition"]["expected"], | |
| operation["precondition"]["mask"], | |
| ) | |
| operations.append( | |
| _WriteOp( | |
| index=index, | |
| resource=resource, | |
| offset=offset, | |
| value=operation["value"], | |
| write_mask=operation.get("write_mask", 0xFFFF_FFFF), | |
| precondition=precondition, | |
| readback=operation.get("readback", True), | |
| kind=kind, | |
| ) | |
| ) | |
| requests.append( | |
| (index, request(resource, offset, AccessClass.WRITE)) | |
| ) | |
| elif kind in ("mmio_poll32", "state_poll32"): | |
| resource = lookup(operation["resource"]) | |
| offset = operation["offset"] | |
| operations.append( | |
| _PollOp( | |
| index=index, | |
| resource=resource, | |
| offset=offset, | |
| expected=operation["expected"], | |
| mask=operation.get("mask", 0xFFFF_FFFF), | |
| interval_ns=operation["interval_ns"], | |
| timeout_ns=operation["timeout_ns"], | |
| kind=kind, | |
| ) | |
| ) | |
| requests.append( | |
| (index, request(resource, offset, AccessClass.POLL)) | |
| ) | |
| elif kind == "sequence": | |
| seq_items: list[SequenceItem] = [] | |
| has_mutation = False | |
| for item in operation["items"]: | |
| ikind = item["kind"] | |
| if ikind in ("mmio_read32", "state_read32"): | |
| res = lookup(item["resource"]) | |
| off = item["offset"] | |
| seq_items.append( | |
| SequenceItem( | |
| kind="read32", | |
| resource=res.id, | |
| offset=off, | |
| ) | |
| ) | |
| requests.append( | |
| (index, request(res, off, AccessClass.SEQUENCE)) | |
| ) | |
| elif ikind in ( | |
| "mmio_write32", | |
| "knob_write32", | |
| "trigger_write32", | |
| ): | |
| res = lookup(item["resource"]) | |
| off = item["offset"] | |
| has_mutation = True | |
| p = None | |
| if ( | |
| "precondition" in item | |
| and item["precondition"] is not None | |
| ): | |
| p = ( | |
| item["precondition"]["expected"], | |
| item["precondition"]["mask"], | |
| ) | |
| seq_items.append( | |
| SequenceItem( | |
| kind="write32", | |
| resource=res.id, | |
| offset=off, | |
| value=item["value"], | |
| write_mask=item.get("write_mask", 0xFFFF_FFFF), | |
| precondition=p, | |
| readback=item.get("readback", True), | |
| ) | |
| ) | |
| requests.append( | |
| (index, request(res, off, AccessClass.WRITE)) | |
| ) | |
| elif ikind in ("mmio_poll32", "state_poll32"): | |
| res = lookup(item["resource"]) | |
| off = item["offset"] | |
| seq_items.append( | |
| SequenceItem( | |
| kind="poll32", | |
| resource=res.id, | |
| offset=off, | |
| expected=item["expected"], | |
| mask=item.get("mask", 0xFFFF_FFFF), | |
| interval_ns=item["interval_ns"], | |
| timeout_ns=item["timeout_ns"], | |
| ) | |
| ) | |
| requests.append( | |
| (index, request(res, off, AccessClass.POLL)) | |
| ) | |
| elif ikind == "delay_ns": | |
| seq_items.append( | |
| SequenceItem( | |
| kind="delay_ns", | |
| delay_ns=item["duration_ns"], | |
| ) | |
| ) | |
| elif ikind == "barrier": | |
| seq_items.append( | |
| SequenceItem( | |
| kind="barrier", | |
| barrier=item.get("variant", "memory"), | |
| ) | |
| ) | |
| operations.append( | |
| _SequenceOp( | |
| index=index, | |
| items=tuple(seq_items), | |
| has_mutation=has_mutation, | |
| ) | |
| ) | |
| return operations, requests | |
| async def _run_direct( | |
| self, | |
| canonical: Mapping[str, Any], | |
| digest: str, | |
| recorder: EvidenceRecorder, | |
| finish: Callable[[int, str | None], Awaitable[RunResult]], | |
| calls: list[Mapping[str, Any]], | |
| ) -> RunResult: | |
| recorder.mark_not_applicable("permission-resolution.json") | |
| recorder.mark_not_applicable("target-audit.jsonl") | |
| context = SessionContext( | |
| run_id=canonical["run_id"], | |
| case_id=canonical["case_id"], | |
| plan_digest=digest, | |
| ) | |
| try: | |
| assert isinstance(self._transport, DirectTransport) | |
| direct_session = await self._transport.open_direct_session(context) | |
| except TransportError as error: | |
| return await finish( | |
| EXIT_TRANSPORT, f"direct connection failed: {error}" | |
| ) | |
| exit_category = EXIT_SUCCESS | |
| failure: str | None = None | |
| operation_rows: list[dict[str, object]] = [] | |
| for index, op in enumerate(canonical["operations"]): | |
| method = op["method"] | |
| args = op.get("args", {}) | |
| try: | |
| outcome = await direct_session.call_fidl(method, args) | |
| row = { | |
| "operation": index, | |
| "kind": "fidl_call", | |
| "method": method, | |
| "args": args, | |
| "response": outcome.response, | |
| "timestamp_ns": outcome.timestamp_ns, | |
| } | |
| operation_rows.append(row) | |
| calls.append(row) | |
| except OperationDenied as error: | |
| operation_rows.append( | |
| { | |
| "operation": index, | |
| "kind": "fidl_call", | |
| "method": method, | |
| "error": error.denial.value, | |
| } | |
| ) | |
| exit_category = EXIT_OPERATION | |
| failure = error.denial.value | |
| break | |
| except TransportError as error: | |
| operation_rows.append( | |
| { | |
| "operation": index, | |
| "kind": "fidl_call", | |
| "method": method, | |
| "error": str(error), | |
| } | |
| ) | |
| exit_category = EXIT_TRANSPORT | |
| failure = f"direct FIDL call failed: {error}" | |
| break | |
| except (KeyboardInterrupt, asyncio.CancelledError): | |
| exit_category = EXIT_OPERATION | |
| failure = "cancelled by operator" | |
| break | |
| recorder.write_jsonl("operations.jsonl", operation_rows) | |
| try: | |
| await direct_session.close() | |
| except TransportError as error: | |
| if failure is None: | |
| exit_category = EXIT_TRANSPORT | |
| failure = f"session close failed: {error}" | |
| return await finish(exit_category, failure) | |
| async def run_plan(self, plan: Mapping[str, object]) -> RunResult: | |
| """Runs one validated plan to a finalized evidence bundle. | |
| Raises `PlanError` for a structurally invalid plan and | |
| `EvidenceError` when the evidence directory cannot be created; | |
| every other failure is reported through the returned | |
| `RunResult` with its evidence finalized. | |
| """ | |
| canonical = validate_plan(plan) | |
| digest = plan_digest(plan) | |
| recorder = EvidenceRecorder(self._evidence_root, canonical["run_id"]) | |
| recorder.write_json("plan.requested.json", dict(plan)) | |
| recorder.write_json( | |
| "plan.canonical.json", {"digest": digest, "plan": canonical} | |
| ) | |
| reads: list[ReadRecord] = [] | |
| writes: list[WriteRecord] = [] | |
| polls: list[PollRecord] = [] | |
| sequences: list[SequenceRecord] = [] | |
| calls: list[Mapping[str, Any]] = [] | |
| serial_session: SerialCaptureSession | None = None | |
| serial_result: SerialCaptureResult | None = None | |
| was_proxy_activated = False | |
| teardown_done = False | |
| access = canonical["access"] | |
| requested_mode = access["mode"] | |
| is_in_situ = ( | |
| requested_mode == "in-situ" | |
| or ( | |
| requested_mode == "proxy" | |
| and access.get("activation") == "in-situ" | |
| ) | |
| or self._driver_moniker is not None | |
| ) | |
| recorded_driver_moniker: str | None = ( | |
| canonical["node"].get("driver_moniker") | |
| or self._driver_moniker | |
| or (canonical["node"]["id"] if is_in_situ else None) | |
| ) | |
| recorded_driver_url: str | None = self._driver_url | |
| async def finish(exit_category: int, failure: str | None) -> RunResult: | |
| nonlocal was_proxy_activated, teardown_done, serial_result | |
| if serial_session is not None and serial_result is None: | |
| try: | |
| serial_result = await serial_session.stop() | |
| recorder.write_bytes("serial.log", serial_result.data) | |
| if serial_result.contains_panic and failure is None: | |
| exit_category = EXIT_TRANSPORT | |
| failure = ( | |
| "target kernel panic detected in serial log: " | |
| f"{serial_result.panic_summary}" | |
| ) | |
| except Exception as exc: | |
| if failure is None: | |
| failure = f"stopping serial capture failed: {exc}" | |
| if was_proxy_activated and not teardown_done: | |
| teardown_done = True | |
| node_id = canonical["node"]["id"] | |
| try: | |
| if self._activator is not None: | |
| await self._activator.end_proxy(node_id) | |
| if self._discovery is not None: | |
| post_desc = await self._discovery.describe_node(node_id) | |
| if post_desc is not None: | |
| recorder.write_json( | |
| "node.after.json", dataclasses.asdict(post_desc) | |
| ) | |
| if not post_desc.is_unclaimed: | |
| if failure is None: | |
| exit_category = EXIT_ACTIVATION | |
| failure = ( | |
| f"verified teardown failed: node {node_id} " | |
| f"still bound to {post_desc.bound_driver_url}" | |
| ) | |
| except Exception as exc: | |
| if failure is None: | |
| exit_category = EXIT_ACTIVATION | |
| failure = f"teardown failed: {exc}" | |
| elif ( | |
| is_in_situ and not teardown_done and self._discovery is not None | |
| ): | |
| teardown_done = True | |
| node_id = canonical["node"]["id"] | |
| try: | |
| post_desc = await self._discovery.describe_node(node_id) | |
| if post_desc is not None: | |
| recorder.write_json( | |
| "node.after.json", dataclasses.asdict(post_desc) | |
| ) | |
| except Exception: | |
| pass | |
| for name in _DEFERRED_ARTIFACTS + _NOT_APPLICABLE_ARTIFACTS: | |
| if not recorder.recorded(name): | |
| recorder.mark_not_applicable(name) | |
| manifest_extra: dict[str, Any] = { | |
| "run_id": canonical["run_id"], | |
| "case_id": canonical["case_id"], | |
| "plan_digest": digest, | |
| "failure": failure, | |
| } | |
| if is_in_situ or recorded_driver_moniker is not None: | |
| manifest_extra["driver_moniker"] = recorded_driver_moniker | |
| manifest_extra["driver_url"] = recorded_driver_url | |
| if serial_result is not None: | |
| manifest_extra[ | |
| "serial_capture" | |
| ] = serial_result.to_manifest_metadata() | |
| recorder.finalize( | |
| exit_category, | |
| manifest_extra, | |
| ) | |
| return RunResult( | |
| exit_category=exit_category, | |
| evidence_dir=recorder.directory, | |
| plan_digest=digest, | |
| reads=tuple(reads), | |
| writes=tuple(writes), | |
| polls=tuple(polls), | |
| sequences=tuple(sequences), | |
| calls=tuple(calls), | |
| failure=failure, | |
| ) | |
| # Mode and guarantee resolution happen before any target | |
| # connection: an unsupported mode or guarantee fails here, never | |
| # by silently substituting a different backend. | |
| is_direct_transport = getattr(self._transport, "is_direct", False) | |
| if requested_mode == "direct": | |
| if not is_direct_transport: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "transport does not support direct mode; a direct-mode plan must never silently execute over the proxy", | |
| ) | |
| capabilities = DIRECT_CAPABILITIES | |
| elif requested_mode == "in-situ": | |
| if is_direct_transport: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "transport does not support in-situ mode", | |
| ) | |
| capabilities = IN_SITU_CAPABILITIES | |
| elif requested_mode == "proxy": | |
| if is_direct_transport: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "transport does not support proxy mode", | |
| ) | |
| capabilities = ( | |
| IN_SITU_CAPABILITIES if is_in_situ else PROXY_CAPABILITIES | |
| ) | |
| else: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| f"access.mode {requested_mode!r} is not implemented", | |
| ) | |
| recorder.write_json("access.capabilities.json", capabilities) | |
| for flag, capability in ( | |
| ("requires_target_policy", "target_policy"), | |
| ("requires_target_audit", "target_audit"), | |
| ("requires_target_local_timing", "target_local_timing"), | |
| ): | |
| if access[flag] and not capabilities[capability]: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| f"plan requires {capability}, which {requested_mode} mode does not provide", | |
| ) | |
| if requested_mode == "direct": | |
| if any( | |
| op["kind"] | |
| in ( | |
| "mmio_read32", | |
| "state_read32", | |
| "mmio_snapshot32", | |
| "mmio_write32", | |
| "knob_write32", | |
| "trigger_write32", | |
| "mmio_poll32", | |
| "state_poll32", | |
| "sequence", | |
| ) | |
| for op in canonical["operations"] | |
| ): | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "direct mode does not provide private MMIO access", | |
| ) | |
| elif requested_mode in ("proxy", "in-situ"): | |
| if any(op["kind"] == "fidl_call" for op in canonical["operations"]): | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| f"{requested_mode} mode does not support fidl_call", | |
| ) | |
| # Spec 14.1 Step 9: Verify independent serial/recovery availability. | |
| requires_recovery = canonical["target"].get("requires_recovery", False) | |
| if requires_recovery: | |
| if self._recovery is None: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "target.requires_recovery specified but no recovery controller configured", | |
| ) | |
| if not await self._recovery.is_available(): | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| "independent recovery channel is unavailable", | |
| ) | |
| requires_serial = canonical["target"].get("requires_serial", False) | |
| if requires_serial: | |
| if self._serial_capture is None: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "target.requires_serial specified but no serial capture configured", | |
| ) | |
| if not await self._serial_capture.is_available(): | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| "serial capture channel is unavailable", | |
| ) | |
| # Spec 14.2 Step 2: Start serial and liveness capture. | |
| if self._serial_capture is not None: | |
| try: | |
| if await self._serial_capture.is_available(): | |
| serial_session = await self._serial_capture.start() | |
| except Exception as exc: | |
| if requires_serial: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"starting serial capture failed: {exc}", | |
| ) | |
| if is_in_situ: | |
| node_id = canonical["node"]["id"] | |
| if self._discovery is not None: | |
| try: | |
| node_desc = await self._discovery.describe_node(node_id) | |
| except DiscoveryError as exc: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node discovery failed for {node_id}: {exc}", | |
| ) | |
| if node_desc is not None: | |
| expected_unclaimed = canonical["node"].get( | |
| "expected_unclaimed", False | |
| ) | |
| if expected_unclaimed and not node_desc.is_unclaimed: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node {node_id} is bound to {node_desc.bound_driver_url}, expected unclaimed", | |
| ) | |
| if not expected_unclaimed and node_desc.is_unclaimed: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node {node_id} is unclaimed, expected active bound driver for in-situ session", | |
| ) | |
| if node_desc.bound_driver_url: | |
| recorded_driver_url = node_desc.bound_driver_url | |
| recorder.write_json( | |
| "node.before.json", dataclasses.asdict(node_desc) | |
| ) | |
| elif "expected_unclaimed" in canonical["node"]: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node {node_id} not found during discovery", | |
| ) | |
| elif "expected_unclaimed" in canonical["node"]: | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "node.expected_unclaimed cannot be verified without node discovery", | |
| ) | |
| elif "expected_unclaimed" in canonical["node"] or ( | |
| requested_mode == "proxy" | |
| and access.get("activation") == "bind-unclaimed" | |
| and self._discovery is not None | |
| and self._activator is not None | |
| ): | |
| if self._discovery is None: | |
| # Node discovery is not configured, so the assertion cannot be | |
| # verified; fail closed rather than running unverified. | |
| return await finish( | |
| EXIT_UNSUPPORTED, | |
| "node.expected_unclaimed cannot be verified without node discovery", | |
| ) | |
| expected_unclaimed = canonical["node"].get( | |
| "expected_unclaimed", True | |
| ) | |
| node_id = canonical["node"]["id"] | |
| try: | |
| node_desc = await self._discovery.describe_node(node_id) | |
| except DiscoveryError as exc: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node discovery failed for {node_id}: {exc}", | |
| ) | |
| if node_desc is None: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node {node_id} not found during discovery", | |
| ) | |
| if expected_unclaimed and not node_desc.is_unclaimed: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node {node_id} is bound to {node_desc.bound_driver_url}; " | |
| "managed takeover is phase 2", | |
| ) | |
| if not expected_unclaimed and node_desc.is_unclaimed: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"node {node_id} is unclaimed, expected bound driver", | |
| ) | |
| recorder.write_json( | |
| "node.before.json", dataclasses.asdict(node_desc) | |
| ) | |
| if ( | |
| not is_in_situ | |
| and requested_mode == "proxy" | |
| and access.get("activation") == "bind-unclaimed" | |
| and self._activator is not None | |
| and self._discovery is not None | |
| ): | |
| node_id = canonical["node"]["id"] | |
| try: | |
| await self._activator.bind_proxy(node_id) | |
| was_proxy_activated = True | |
| except Exception as error: | |
| return await finish( | |
| EXIT_ACTIVATION, | |
| f"proxy activation failed: {error}", | |
| ) | |
| # Prepare: describe, freeze expectations, resolve consent. No | |
| # hardware access happens in this phase. | |
| try: | |
| description = await self._transport.describe() | |
| except TransportError as error: | |
| return await finish(EXIT_TRANSPORT, f"describe failed: {error}") | |
| recorder.write_json( | |
| "target.description.json", dataclasses.asdict(description) | |
| ) | |
| expected_boot_id = canonical["target"].get("expected_boot_id") | |
| if ( | |
| expected_boot_id is not None | |
| and expected_boot_id != description.boot_id | |
| ): | |
| return await finish( | |
| EXIT_STALE, "expected_boot_id does not match target" | |
| ) | |
| expected_digest = canonical["node"].get("expected_resource_digest") | |
| if expected_digest is not None: | |
| if requested_mode == "direct": | |
| return await finish( | |
| EXIT_STALE, | |
| "direct mode target has no proxy resource digest", | |
| ) | |
| assert isinstance(description, ProxyDescription) | |
| if expected_digest != description.resource_digest: | |
| return await finish( | |
| EXIT_STALE, "expected_resource_digest does not match target" | |
| ) | |
| if requested_mode == "direct": | |
| return await self._run_direct( | |
| canonical, digest, recorder, finish, calls | |
| ) | |
| assert isinstance(description, ProxyDescription) | |
| assert isinstance(self._transport, ProxyTransport) | |
| try: | |
| operations, requests = self._derive(canonical, description) | |
| except _StaleResource as error: | |
| return await finish(EXIT_STALE, str(error)) | |
| try: | |
| grants = load_grants(self._grants_path) | |
| except GrantStoreError as error: | |
| return await finish(EXIT_PERMISSION, f"grant store error: {error}") | |
| resolved: list[tuple[int, AccessRequest, Resolution]] = [] | |
| undecided: dict[ | |
| tuple[str, str, str, str, int, int, AccessClass], AccessRequest | |
| ] = {} | |
| persistent_denied = False | |
| for index, access_request in requests: | |
| resolution = resolve(access_request, grants) | |
| resolved.append((index, access_request, resolution)) | |
| if resolution.outcome is Outcome.DENIED: | |
| persistent_denied = True | |
| elif resolution.outcome is Outcome.UNDECIDED: | |
| undecided.setdefault(access_request.match_key, access_request) | |
| # A persistent deny short-circuits prompting: the operator already | |
| # made a durable decision, so nobody is asked to re-litigate it. | |
| prompted: dict[ | |
| tuple[str, str, str, str, int, int, AccessClass], ConsentDecision | |
| ] = {} | |
| if not persistent_denied and undecided and self._consent is not None: | |
| for access_request in undecided.values(): | |
| warning = ( | |
| WRITE_WARNING | |
| if access_request.access == AccessClass.WRITE | |
| else READ_WARNING | |
| ) | |
| decision = await self._consent.request_consent( | |
| access_request, warning | |
| ) | |
| prompted[access_request.match_key] = decision | |
| if decision in ( | |
| ConsentDecision.ALWAYS_ALLOW, | |
| ConsentDecision.ALWAYS_DENY, | |
| ): | |
| if ( | |
| decision == ConsentDecision.ALWAYS_ALLOW | |
| and access_request.access | |
| in (AccessClass.WRITE, AccessClass.SEQUENCE) | |
| ): | |
| return await finish( | |
| EXIT_PERMISSION, | |
| f"persistent {access_request.access.value} grants are not supported", | |
| ) | |
| try: | |
| add_grant( | |
| self._grants_path, | |
| grant_from_decision(access_request, decision), | |
| ) | |
| except (GrantStoreError, ValueError) as error: | |
| return await finish( | |
| EXIT_PERMISSION, f"grant store error: {error}" | |
| ) | |
| resolution_rows: list[dict[str, object]] = [] | |
| operator_denied = False | |
| fail_closed = False | |
| for index, access_request, resolution in resolved: | |
| grant_id: str | None = None | |
| if resolution.decided: | |
| outcome_value = resolution.outcome.value | |
| source = "persistent" | |
| grant_id = ( | |
| resolution.grant.grant_id if resolution.grant else None | |
| ) | |
| elif access_request.match_key in prompted: | |
| decision = prompted[access_request.match_key] | |
| allowed = decision in ( | |
| ConsentDecision.ALLOW_ONCE, | |
| ConsentDecision.ALWAYS_ALLOW, | |
| ) | |
| outcome_value = ( | |
| Outcome.ALLOWED if allowed else Outcome.DENIED | |
| ).value | |
| operator_denied = operator_denied or not allowed | |
| source = decision.value | |
| else: | |
| outcome_value = Outcome.UNDECIDED.value | |
| source = "fail_closed" | |
| fail_closed = True | |
| resolution_rows.append( | |
| { | |
| "operation": index, | |
| "resource": access_request.resource, | |
| "offset": access_request.offset, | |
| "access": access_request.access.value, | |
| "outcome": outcome_value, | |
| "source": source, | |
| "grant_id": grant_id, | |
| } | |
| ) | |
| recorder.write_json("permission-resolution.json", resolution_rows) | |
| if persistent_denied: | |
| return await finish(EXIT_PERMISSION, "denied by persistent grant") | |
| if operator_denied: | |
| return await finish(EXIT_PERMISSION, "denied by operator") | |
| if fail_closed: | |
| return await finish( | |
| EXIT_PERMISSION, | |
| "consent required; unattended operation fails closed", | |
| ) | |
| allowlist = sorted( | |
| { | |
| AllowRule( | |
| resource=description.resource_named(req.resource).id, # type: ignore[union-attr] | |
| offset=req.offset, | |
| width=req.width, | |
| access=req.access, | |
| ) | |
| for _, req in requests | |
| }, | |
| key=lambda rule: (rule.resource, rule.offset, rule.access.value), | |
| ) | |
| # Execute: open the session with the exact allowlist and session mode, | |
| # run the bounded operations in order, then drain audit before closing. | |
| mutating = is_mutating_plan(canonical) | |
| session_mode = ( | |
| SessionMode.MUTATING if mutating else SessionMode.READ_ONLY | |
| ) | |
| context = SessionContext( | |
| run_id=canonical["run_id"], | |
| case_id=canonical["case_id"], | |
| plan_digest=digest, | |
| ) | |
| try: | |
| session = await self._transport.open_session( | |
| context, | |
| description.expectations(), | |
| allowlist, | |
| mode=session_mode, | |
| ) | |
| except OpenSessionRejected as error: | |
| category = ( | |
| EXIT_STALE | |
| if error.reason in STALE_REJECTIONS | |
| else EXIT_ACTIVATION | |
| ) | |
| return await finish( | |
| category, f"session rejected: {error.reason.value}" | |
| ) | |
| except TransportError as error: | |
| return await finish(EXIT_TRANSPORT, f"open_session failed: {error}") | |
| exit_category = EXIT_SUCCESS | |
| failure: str | None = None | |
| operation_rows: list[dict[str, object]] = [] | |
| audit_rows: list[dict[str, object]] = [] | |
| cursor = 0 | |
| async def drain_audit() -> None: | |
| nonlocal cursor | |
| while True: | |
| page = await session.read_audit(cursor, 64) | |
| if not page.entries: | |
| break | |
| audit_rows.extend(entry.to_json() for entry in page.entries) | |
| cursor = page.next_cursor | |
| for operation in operations: | |
| try: | |
| if isinstance(operation, _ReadOp): | |
| read_outcome = await session.read32( | |
| operation.resource.id, operation.offset | |
| ) | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": operation.kind, | |
| "resource": operation.resource.name, | |
| "offset": operation.offset, | |
| "value": read_outcome.value, | |
| "audit_seq": read_outcome.audit_seq, | |
| "timestamp_ns": read_outcome.timestamp_ns, | |
| } | |
| ) | |
| reads.append( | |
| ReadRecord( | |
| operation_index=operation.index, | |
| resource=operation.resource.name, | |
| offset=operation.offset, | |
| value=read_outcome.value, | |
| audit_seq=read_outcome.audit_seq, | |
| timestamp_ns=read_outcome.timestamp_ns, | |
| ) | |
| ) | |
| elif isinstance(operation, _SnapshotOp): | |
| items = [ | |
| SnapshotItem(resource=resource.id, offset=offset) | |
| for resource, offset in operation.items | |
| ] | |
| snapshot = await session.snapshot(items) | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": "mmio_snapshot32", | |
| "complete": snapshot.complete, | |
| "results": [ | |
| { | |
| "resource": result.item.resource, | |
| "offset": result.item.offset, | |
| "ok": result.ok, | |
| "value": ( | |
| result.value if result.ok else None | |
| ), | |
| "audit_seq": result.audit_seq, | |
| "timestamp_ns": result.timestamp_ns, | |
| } | |
| for result in snapshot.results | |
| ], | |
| } | |
| ) | |
| if not snapshot.complete: | |
| exit_category = EXIT_OPERATION | |
| failure = "snapshot stopped at a backend failure" | |
| break | |
| elif isinstance(operation, _WriteOp): | |
| write_outcome = await session.write32( | |
| operation.resource.id, | |
| operation.offset, | |
| operation.value, | |
| write_mask=operation.write_mask, | |
| precondition=operation.precondition, | |
| readback=operation.readback, | |
| ) | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": operation.kind, | |
| "resource": operation.resource.name, | |
| "offset": operation.offset, | |
| "value": operation.value, | |
| "write_mask": operation.write_mask, | |
| "readback_value": write_outcome.readback_value, | |
| "audit_seq": write_outcome.audit_seq, | |
| "timestamp_ns": write_outcome.timestamp_ns, | |
| } | |
| ) | |
| writes.append( | |
| WriteRecord( | |
| operation_index=operation.index, | |
| resource=operation.resource.name, | |
| offset=operation.offset, | |
| value=operation.value, | |
| write_mask=operation.write_mask, | |
| readback_value=write_outcome.readback_value, | |
| audit_seq=write_outcome.audit_seq, | |
| timestamp_ns=write_outcome.timestamp_ns, | |
| ) | |
| ) | |
| # Spec 14.2 step 6: drain audit immediately after each write. | |
| await drain_audit() | |
| elif isinstance(operation, _PollOp): | |
| poll_outcome = await session.poll32( | |
| operation.resource.id, | |
| operation.offset, | |
| operation.expected, | |
| mask=operation.mask, | |
| interval_ns=operation.interval_ns, | |
| timeout_ns=operation.timeout_ns, | |
| ) | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": operation.kind, | |
| "resource": operation.resource.name, | |
| "offset": operation.offset, | |
| "expected": operation.expected, | |
| "mask": operation.mask, | |
| "value": poll_outcome.value, | |
| "audit_seq": poll_outcome.audit_seq, | |
| "timestamp_ns": poll_outcome.timestamp_ns, | |
| } | |
| ) | |
| polls.append( | |
| PollRecord( | |
| operation_index=operation.index, | |
| resource=operation.resource.name, | |
| offset=operation.offset, | |
| expected=operation.expected, | |
| mask=operation.mask, | |
| value=poll_outcome.value, | |
| audit_seq=poll_outcome.audit_seq, | |
| timestamp_ns=poll_outcome.timestamp_ns, | |
| ) | |
| ) | |
| elif isinstance(operation, _SequenceOp): | |
| seq_outcome = await session.execute_sequence( | |
| operation.items | |
| ) | |
| item_records = [ | |
| SequenceItemRecord( | |
| index=res.index, | |
| ok=res.ok, | |
| kind=res.kind, | |
| value=res.value, | |
| readback_value=res.readback_value, | |
| audit_seq=res.audit_seq, | |
| timestamp_ns=res.timestamp_ns, | |
| error=res.error.value | |
| if res.error is not None | |
| else None, | |
| ) | |
| for res in seq_outcome.results | |
| ] | |
| seq_record = SequenceRecord( | |
| operation_index=operation.index, | |
| results=tuple(item_records), | |
| complete=seq_outcome.complete, | |
| ) | |
| sequences.append(seq_record) | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": "sequence", | |
| "complete": seq_outcome.complete, | |
| "results": [ | |
| { | |
| "index": r.index, | |
| "ok": r.ok, | |
| "kind": r.kind, | |
| "value": r.value, | |
| "readback_value": r.readback_value, | |
| "audit_seq": r.audit_seq, | |
| "timestamp_ns": r.timestamp_ns, | |
| "error": r.error, | |
| } | |
| for r in item_records | |
| ], | |
| } | |
| ) | |
| if operation.has_mutation: | |
| # Spec 14.2 step 6: drain audit immediately after mutating sequence. | |
| await drain_audit() | |
| if not seq_outcome.complete: | |
| exit_category = EXIT_OPERATION | |
| failure = "sequence stopped at an item failure" | |
| break | |
| except OperationDenied as error: | |
| kind = "unknown" | |
| if isinstance(operation, (_ReadOp, _WriteOp, _PollOp)): | |
| kind = operation.kind | |
| elif isinstance(operation, _SnapshotOp): | |
| kind = "mmio_snapshot32" | |
| elif isinstance(operation, _SequenceOp): | |
| kind = "sequence" | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": kind, | |
| "error": error.denial.value, | |
| } | |
| ) | |
| exit_category = EXIT_OPERATION | |
| failure = error.denial.value | |
| break | |
| except TransportError as error: | |
| kind = "unknown" | |
| if isinstance(operation, (_ReadOp, _WriteOp, _PollOp)): | |
| kind = operation.kind | |
| elif isinstance(operation, _SnapshotOp): | |
| kind = "mmio_snapshot32" | |
| elif isinstance(operation, _SequenceOp): | |
| kind = "sequence" | |
| operation_rows.append( | |
| { | |
| "operation": operation.index, | |
| "kind": kind, | |
| "error": str(error), | |
| } | |
| ) | |
| exit_category = EXIT_TRANSPORT | |
| failure = f"operation failed: {error}" | |
| if serial_session is not None: | |
| try: | |
| curr_log = await serial_session.get_current_log() | |
| if detect_panic(curr_log): | |
| panic_sum = extract_panic_summary(curr_log) | |
| failure = ( | |
| "target kernel panic detected: " f"{panic_sum}" | |
| ) | |
| except Exception: | |
| pass | |
| if self._recovery is not None: | |
| try: | |
| if not await self._recovery.check_liveness(): | |
| if "panic" not in str(failure): | |
| failure = ( | |
| f"target liveness check failed: {failure}" | |
| ) | |
| except Exception: | |
| pass | |
| break | |
| except (KeyboardInterrupt, asyncio.CancelledError): | |
| # Operator cancellation stops execution but never skips | |
| # audit draining, session close, or evidence finalization. | |
| exit_category = EXIT_OPERATION | |
| failure = "cancelled by operator" | |
| break | |
| recorder.write_jsonl("operations.jsonl", operation_rows) | |
| # Drain audit and close even after an operation failure or a | |
| # cancellation so partial runs still produce complete evidence. | |
| try: | |
| await drain_audit() | |
| await session.close() | |
| except ( | |
| TransportError, | |
| KeyboardInterrupt, | |
| asyncio.CancelledError, | |
| ) as error: | |
| if failure is None: | |
| exit_category = EXIT_TRANSPORT | |
| failure = f"audit drain failed: {error}" | |
| recorder.write_jsonl("target-audit.jsonl", audit_rows) | |
| return await finish(exit_category, failure) | |
| async def connect( | |
| target: str = "default", | |
| *, | |
| transport: str = "auto", | |
| timeout_s: float = 10.0, | |
| **kwargs: Any, | |
| ) -> DriverLab: | |
| """Connects to a target, returning a configured DriverLab instance (Spec Section 9).""" | |
| return await DriverLab.connect( | |
| target=target, transport=transport, timeout_s=timeout_s, **kwargs | |
| ) |