288 lines
11 KiB
Python
288 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Offline Phase-1.0AA integration for the passive batch contract.
|
|
|
|
Only the exact built-in fake adapter and synthetic clock are accepted. This
|
|
module has no live adapter protocol, network import, address, CLI or real clock.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
import math
|
|
from typing import Any
|
|
|
|
from phase10w_shsrv_client_policy import SessionPlan
|
|
from phase10x_inactive_transport import (
|
|
EvidenceFailure,
|
|
EvidenceRecord,
|
|
ExclusiveEvidenceStore,
|
|
)
|
|
from phase10z_passive_batch_contract import (
|
|
PassiveBatch,
|
|
PassiveContractError,
|
|
PassiveResultAccumulator,
|
|
build_passive_batch,
|
|
)
|
|
|
|
|
|
PHASE10Z_CONTRACT_SHA256 = \
|
|
"0728c2be7f368e0a7f4b68efe86f6e0c5c2f50704a41d0e1992b0bfec19dde06"
|
|
MAX_FAKE_EVENTS = 257
|
|
MAX_FAKE_ADVANCE_SECONDS = 60.0
|
|
EVENT_DATA = "DATA"
|
|
EVENT_HARD_DEADLINE = "HARD_DEADLINE"
|
|
EVENT_REMOTE_EOF = "REMOTE_EOF"
|
|
EVENT_BLOCKED = "BLOCKED"
|
|
EVENT_KINDS = {
|
|
EVENT_DATA, EVENT_HARD_DEADLINE, EVENT_REMOTE_EOF, EVENT_BLOCKED,
|
|
}
|
|
|
|
|
|
class OfflineIntegrationError(RuntimeError):
|
|
"""Normalized offline failure without supplied data, target or path."""
|
|
|
|
|
|
class FakeAdapterError(RuntimeError):
|
|
"""Built-in fake-adapter state failure."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FakeReceiveEvent:
|
|
"""One caller-supplied synthetic event; never a live receive result."""
|
|
|
|
kind: str
|
|
data: bytes = b""
|
|
advance_seconds: float = 0.0
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.kind not in EVENT_KINDS:
|
|
raise FakeAdapterError("fake event kind is invalid")
|
|
if not isinstance(self.data, bytes):
|
|
raise FakeAdapterError("fake event data must be bytes")
|
|
if self.kind == EVENT_DATA and not self.data:
|
|
raise FakeAdapterError("fake data event is empty")
|
|
if self.kind != EVENT_DATA and self.data:
|
|
raise FakeAdapterError("fake control event contains data")
|
|
value = self.advance_seconds
|
|
if not isinstance(value, (int, float)) or isinstance(value, bool) or \
|
|
not math.isfinite(value) or not 0.0 <= value <= \
|
|
MAX_FAKE_ADVANCE_SECONDS:
|
|
raise FakeAdapterError("fake time advance is invalid")
|
|
|
|
|
|
class OfflineFakeClock:
|
|
"""Explicit synthetic monotonic value; never acquires host time."""
|
|
|
|
def __init__(self, initial: float = 0.0) -> None:
|
|
if not isinstance(initial, (int, float)) or isinstance(initial, bool) or \
|
|
not math.isfinite(initial):
|
|
raise FakeAdapterError("fake clock initial value is invalid")
|
|
self._value = float(initial)
|
|
|
|
def monotonic(self) -> float:
|
|
return self._value
|
|
|
|
def advance(self, seconds: float) -> None:
|
|
if not isinstance(seconds, (int, float)) or isinstance(seconds, bool) or \
|
|
not math.isfinite(seconds) or not 0.0 <= seconds <= \
|
|
MAX_FAKE_ADVANCE_SECONDS:
|
|
raise FakeAdapterError("fake clock advance is invalid")
|
|
self._value += float(seconds)
|
|
|
|
|
|
class OfflineFakeBatchAdapter:
|
|
"""Closed fake only: one open, one exact batch and scripted events."""
|
|
|
|
def __init__(
|
|
self, clock: OfflineFakeClock, events: tuple[FakeReceiveEvent, ...],
|
|
) -> None:
|
|
if type(clock) is not OfflineFakeClock:
|
|
raise FakeAdapterError("only the exact fake clock is accepted")
|
|
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_FAKE_EVENTS:
|
|
raise FakeAdapterError("fake event sequence is invalid")
|
|
if any(type(event) is not FakeReceiveEvent for event in events):
|
|
raise FakeAdapterError("fake event type is invalid")
|
|
self.clock = clock
|
|
self._events = list(events)
|
|
self.state = "NEW"
|
|
self.open_count = 0
|
|
self.send_count = 0
|
|
self.close_count = 0
|
|
self.sent_sha256: str | None = None
|
|
self.trace: list[str] = []
|
|
self.logical_event_buffer_discarded = False
|
|
|
|
def open_once(self) -> None:
|
|
if self.state != "NEW":
|
|
raise FakeAdapterError("fake adapter cannot open")
|
|
self.open_count += 1
|
|
self.trace.append("FAKE_OPEN")
|
|
self.state = "OPEN"
|
|
|
|
def send_one_batch(self, batch: PassiveBatch) -> None:
|
|
if self.state != "OPEN" or type(batch) is not PassiveBatch:
|
|
raise FakeAdapterError("fake adapter cannot accept a batch")
|
|
self.send_count += 1
|
|
if self.send_count != 1:
|
|
raise FakeAdapterError("second fake send is forbidden")
|
|
self.sent_sha256 = hashlib.sha256(batch.payload).hexdigest()
|
|
self.trace.append("FAKE_SEND_ONE_BATCH")
|
|
self.state = "SENT"
|
|
|
|
def next_event(self) -> FakeReceiveEvent:
|
|
if self.state not in {"SENT", "RECEIVING"} or not self._events:
|
|
raise FakeAdapterError("fake adapter has no next event")
|
|
event = self._events.pop(0)
|
|
self.clock.advance(event.advance_seconds)
|
|
self.trace.append(f"FAKE_EVENT_{event.kind}")
|
|
self.state = "RECEIVING"
|
|
return event
|
|
|
|
def close_once(self) -> None:
|
|
if self.state not in {"OPEN", "SENT", "RECEIVING"}:
|
|
raise FakeAdapterError("fake adapter cannot close")
|
|
self.close_count += 1
|
|
if self.close_count != 1:
|
|
raise FakeAdapterError("second fake close is forbidden")
|
|
self._events.clear()
|
|
self.logical_event_buffer_discarded = True
|
|
self.trace.append("FAKE_CLOSE")
|
|
self.state = "CLOSED"
|
|
|
|
|
|
class OfflineFakeEvidenceStore(ExclusiveEvidenceStore):
|
|
"""Phase-specific exclusive local evidence; no raw transcript."""
|
|
|
|
def create_fake_consumed_receipt(
|
|
self, plan: SessionPlan, batch: PassiveBatch, start: float,
|
|
) -> EvidenceRecord:
|
|
return self._create(f"{plan.run_id}.aa-consumed.json", {
|
|
"schema_version": 1,
|
|
"status": "OFFLINE_FAKE_ATTEMPT_CONSUMED_BEFORE_OPEN",
|
|
"run_id": plan.run_id,
|
|
"phase10z_contract_sha256": PHASE10Z_CONTRACT_SHA256,
|
|
"window": batch.window,
|
|
"batch_sha256": hashlib.sha256(batch.payload).hexdigest(),
|
|
"batch_size": len(batch.payload),
|
|
"deadline_seconds": batch.deadline_seconds,
|
|
"created_fake_monotonic": start,
|
|
"target_retained": False,
|
|
"raw_transcript_persisted": False,
|
|
"retry_allowed": False,
|
|
"reconnect_allowed": False,
|
|
"resume_allowed": False,
|
|
"device_behavior_proven": False,
|
|
})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OfflineFakeOutcome:
|
|
receipt: EvidenceRecord
|
|
output: EvidenceRecord
|
|
classification: str
|
|
exact_identity: bool
|
|
batch_sha256: str
|
|
trace: tuple[str, ...]
|
|
fake_only: bool = True
|
|
device_behavior_proven: bool = False
|
|
|
|
|
|
def run_offline_fake_batch(
|
|
plan: SessionPlan,
|
|
adapter: OfflineFakeBatchAdapter,
|
|
clock: OfflineFakeClock,
|
|
evidence: OfflineFakeEvidenceStore,
|
|
) -> OfflineFakeOutcome:
|
|
"""Exercise Z end-to-end with exact built-in fakes and local evidence."""
|
|
if type(adapter) is not OfflineFakeBatchAdapter or \
|
|
type(clock) is not OfflineFakeClock or \
|
|
type(evidence) is not OfflineFakeEvidenceStore or \
|
|
adapter.clock is not clock:
|
|
raise OfflineIntegrationError("offline fake boundary type is invalid")
|
|
try:
|
|
batch = build_passive_batch(plan)
|
|
except PassiveContractError as error:
|
|
raise OfflineIntegrationError("passive batch preparation failed") from error
|
|
start = clock.monotonic()
|
|
deadline = start + batch.deadline_seconds
|
|
try:
|
|
receipt = evidence.create_fake_consumed_receipt(plan, batch, start)
|
|
except EvidenceFailure as error:
|
|
raise OfflineIntegrationError("offline receipt creation failed") from error
|
|
trace = ["RECEIPT_CREATED"]
|
|
accumulator = PassiveResultAccumulator(batch)
|
|
sanitized: dict[str, Any] | None = None
|
|
opened = False
|
|
failure: OfflineIntegrationError | None = None
|
|
try:
|
|
opened = True
|
|
adapter.open_once()
|
|
trace.extend(adapter.trace[-1:])
|
|
if clock.monotonic() >= deadline:
|
|
raise OfflineIntegrationError("deadline reached before fake send")
|
|
adapter.send_one_batch(batch)
|
|
trace.extend(adapter.trace[-1:])
|
|
for _index in range(MAX_FAKE_EVENTS):
|
|
event = adapter.next_event()
|
|
trace.extend(adapter.trace[-1:])
|
|
now = clock.monotonic()
|
|
if event.kind == EVENT_DATA:
|
|
if now >= deadline:
|
|
raise OfflineIntegrationError("fake data reached deadline")
|
|
accumulator.feed_supplied_chunk(event.data)
|
|
continue
|
|
if event.kind == EVENT_HARD_DEADLINE:
|
|
if now < deadline:
|
|
raise OfflineIntegrationError("fake deadline arrived early")
|
|
sanitized = accumulator.seal_at_hard_deadline(True)
|
|
break
|
|
if event.kind == EVENT_REMOTE_EOF:
|
|
raise OfflineIntegrationError("remote EOF is not completion")
|
|
if event.kind == EVENT_BLOCKED:
|
|
raise OfflineIntegrationError("blocked fake receive is invalid")
|
|
if sanitized is None:
|
|
raise OfflineIntegrationError("hard deadline result is missing")
|
|
except Exception as error: # noqa: BLE001 - exact fake boundary normalization
|
|
failure = OfflineIntegrationError("offline fake integration failed")
|
|
failure.__cause__ = error
|
|
finally:
|
|
if opened:
|
|
try:
|
|
adapter.close_once()
|
|
trace.extend(adapter.trace[-1:])
|
|
except FakeAdapterError as error:
|
|
if failure is None:
|
|
failure = OfflineIntegrationError("offline fake close failed")
|
|
failure.__cause__ = error
|
|
if failure is not None:
|
|
raise failure
|
|
if sanitized is None or adapter.sent_sha256 is None:
|
|
raise OfflineIntegrationError("offline fake result is incomplete")
|
|
sanitized["phase10aa_fake_integration"] = {
|
|
"offline_fake_only": True,
|
|
"exact_builtin_adapter_required": True,
|
|
"exact_builtin_clock_required": True,
|
|
"receipt_created_before_fake_open": True,
|
|
"one_fake_open": adapter.open_count == 1,
|
|
"one_fake_batch_send": adapter.send_count == 1,
|
|
"one_fake_close": adapter.close_count == 1,
|
|
"logical_event_buffer_discarded": adapter.logical_event_buffer_discarded,
|
|
"physical_memory_erasure_proven": False,
|
|
"network_transport_present": False,
|
|
"device_behavior_proven": False,
|
|
}
|
|
try:
|
|
output = evidence.create_sanitized_output(plan, receipt, sanitized)
|
|
except EvidenceFailure as error:
|
|
raise OfflineIntegrationError("offline sanitized output failed") from error
|
|
return OfflineFakeOutcome(
|
|
receipt=receipt,
|
|
output=output,
|
|
classification=str(sanitized["classification"]),
|
|
exact_identity=bool(sanitized["exact_identity"]),
|
|
batch_sha256=adapter.sent_sha256,
|
|
trace=tuple(trace),
|
|
)
|