feat(perception): add scoped stream control and bounded clock observations
This commit is contained in:
@@ -0,0 +1,312 @@
|
|||||||
|
"""CPU-only clock/grant route evidence; NOT a model/freshness qualification.
|
||||||
|
|
||||||
|
One bootstrap ticket/certificate copied via authenticated SSH, subsequent data
|
||||||
|
grants delivered only by TLS Poll. Source events/resume proof remain synthetic;
|
||||||
|
real monotonic clock observations use explicit CONDITIONAL error/rate budgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import resource
|
||||||
|
import secrets
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from hashlib import sha256
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
from grpc_transport_probe import document, identity, until
|
||||||
|
|
||||||
|
from k1link.compute.live_perception import LiveIngressEvent
|
||||||
|
from k1link.perception.realtime_contract import StreamStart
|
||||||
|
from k1link.perception.streaming_clock import ClockMappingError, ClockWindow
|
||||||
|
from k1link.perception.streaming_continuity import ResumeEvidence
|
||||||
|
from k1link.perception.streaming_control_grpc import (
|
||||||
|
ControlTicket,
|
||||||
|
StreamControlClient,
|
||||||
|
StreamControlEndpoint,
|
||||||
|
)
|
||||||
|
from k1link.perception.streaming_grpc import GrpcStreamClient, GrpcStreamEndpoint
|
||||||
|
from k1link.perception.streaming_lifecycle import StreamingLifecycle
|
||||||
|
from k1link.perception.streaming_queue import StreamMailbox
|
||||||
|
|
||||||
|
|
||||||
|
async def server(root):
|
||||||
|
private = root / "private"
|
||||||
|
private.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
cert, key, bootstrap = [private / x for x in ("cert.pem", "key.pem", "bootstrap.json")]
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"openssl",
|
||||||
|
"req",
|
||||||
|
"-x509",
|
||||||
|
"-newkey",
|
||||||
|
"rsa:2048",
|
||||||
|
"-nodes",
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
|
"-subj",
|
||||||
|
"/CN=localhost",
|
||||||
|
"-keyout",
|
||||||
|
str(key),
|
||||||
|
"-out",
|
||||||
|
str(cert),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
run = StreamingLifecycle(
|
||||||
|
identity(),
|
||||||
|
root / "lease",
|
||||||
|
StreamMailbox(),
|
||||||
|
threading.Event(),
|
||||||
|
recover_input=True,
|
||||||
|
source_clock_ns=lambda: 1_000_000_000,
|
||||||
|
)
|
||||||
|
resident = run.spawn(
|
||||||
|
lambda: subprocess.Popen(
|
||||||
|
[sys.executable, "-c", "import time; time.sleep(180)"],
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
run.ready()
|
||||||
|
pulse_stop = threading.Event()
|
||||||
|
|
||||||
|
def pulse():
|
||||||
|
while not pulse_stop.wait(0.1):
|
||||||
|
run.renew(identity())
|
||||||
|
|
||||||
|
pulse_thread = threading.Thread(target=pulse, daemon=True)
|
||||||
|
pulse_thread.start()
|
||||||
|
received = []
|
||||||
|
|
||||||
|
def consume(event):
|
||||||
|
item = {
|
||||||
|
"sequence": event.ingress_sequence,
|
||||||
|
"sha256": sha256(event.payload).hexdigest(),
|
||||||
|
"bytes": len(event.payload),
|
||||||
|
"resident_pid": resident.pid,
|
||||||
|
}
|
||||||
|
received.append(item)
|
||||||
|
if len(received) > 8:
|
||||||
|
raise ValueError("bounded count exceeded")
|
||||||
|
endpoint.publish(
|
||||||
|
run.continuity.epoch, event.ingress_sequence, json.dumps(item, sort_keys=True).encode()
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoint = GrpcStreamEndpoint(run, consume, lambda _: None)
|
||||||
|
pending = [endpoint.issue(run.continuity.epoch, "synthetic-capture", 1)]
|
||||||
|
ticket = ControlTicket(identity(), secrets.token_hex(32))
|
||||||
|
clock_id = "worker-process-" + secrets.token_hex(12)
|
||||||
|
control = StreamControlEndpoint(
|
||||||
|
ticket,
|
||||||
|
lambda: pending[0] if endpoint.grant is not None else None,
|
||||||
|
clock_id=clock_id,
|
||||||
|
)
|
||||||
|
report = {
|
||||||
|
"utc": datetime.now(UTC).isoformat(),
|
||||||
|
"started_monotonic_ns": str(time.monotonic_ns()),
|
||||||
|
"model_count": 0,
|
||||||
|
"synthetic_source_and_resume": True,
|
||||||
|
"real_time_qualified": False,
|
||||||
|
"actuation_allowed": False,
|
||||||
|
"received": received,
|
||||||
|
"clock_id": clock_id,
|
||||||
|
"grpc_version": grpc.__version__,
|
||||||
|
}
|
||||||
|
rpc = None
|
||||||
|
try:
|
||||||
|
rpc, _ = await endpoint.serve(
|
||||||
|
"0.0.0.0:50061",
|
||||||
|
certificate=cert.read_bytes(),
|
||||||
|
private_key=key.read_bytes(),
|
||||||
|
control_handlers=(control.handler(),),
|
||||||
|
)
|
||||||
|
document(
|
||||||
|
bootstrap,
|
||||||
|
{
|
||||||
|
"activation": identity().to_dict(),
|
||||||
|
"token": ticket.token,
|
||||||
|
"remote_clock_id": clock_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bootstrap.chmod(0o600)
|
||||||
|
await until(lambda: len(received) == 4 and endpoint.active is None, seconds=90)
|
||||||
|
report["first_transport"] = endpoint.last
|
||||||
|
report["before_gap"] = run.snapshot()
|
||||||
|
began = time.monotonic_ns()
|
||||||
|
await asyncio.sleep(2.2)
|
||||||
|
report["gap_ns"] = str(time.monotonic_ns() - began)
|
||||||
|
assert resident.poll() is None and run.continuity.phase == "waiting"
|
||||||
|
report["after_gap"] = run.snapshot()
|
||||||
|
epoch = run.begin_input(identity())
|
||||||
|
run.resume_input(epoch, ResumeEvidence(*([1_000_000_000] * 4), True), lambda: None)
|
||||||
|
pending[0] = endpoint.issue(epoch, "synthetic-capture", 1)
|
||||||
|
await until(lambda: len(received) == 8 and run.mailbox.done, seconds=30)
|
||||||
|
endpoint.finish_results(epoch)
|
||||||
|
await until(lambda: endpoint.active is None)
|
||||||
|
report["second_transport"] = endpoint.last
|
||||||
|
report["passed"] = resident.poll() is None
|
||||||
|
report["peak_mailbox_bytes"] = run.mailbox.peak_bytes
|
||||||
|
finally:
|
||||||
|
if rpc:
|
||||||
|
await rpc.stop(0)
|
||||||
|
pulse_stop.set()
|
||||||
|
pulse_thread.join(1)
|
||||||
|
report["closed"] = run.close()
|
||||||
|
report["resident_reaped"] = resident.poll() is not None
|
||||||
|
report["final_mailbox_bytes"] = run.mailbox.bytes
|
||||||
|
report["control_accepted"] = control.accepted
|
||||||
|
report["control_rejected"] = control.rejected
|
||||||
|
report["rss_max_kib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||||
|
report["finished_monotonic_ns"] = str(time.monotonic_ns())
|
||||||
|
document(root / "server-report.json", report)
|
||||||
|
for path in (key, bootstrap):
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def client(root, target):
|
||||||
|
bootstrap = root / "bootstrap.json"
|
||||||
|
value = json.loads(bootstrap.read_text())
|
||||||
|
ticket = ControlTicket(StreamStart.from_dict(value["activation"]), value["token"])
|
||||||
|
local_clock_id = "mac-process-" + secrets.token_hex(12)
|
||||||
|
control = StreamControlClient(
|
||||||
|
target, (root / "cert.pem").read_bytes(), ticket, clock_id=local_clock_id
|
||||||
|
)
|
||||||
|
mapping = ClockWindow(
|
||||||
|
local_clock_id,
|
||||||
|
value["remote_clock_id"],
|
||||||
|
rate_ppm=500,
|
||||||
|
timestamp_error_ns=50_000,
|
||||||
|
maximum_age_ns=2_000_000_000,
|
||||||
|
)
|
||||||
|
rows, grants, replies, errors = [], [], [], []
|
||||||
|
report = {
|
||||||
|
"utc": datetime.now(UTC).isoformat(),
|
||||||
|
"started_monotonic_ns": str(time.monotonic_ns()),
|
||||||
|
"schema_version": "missioncore.grpc-control-clock-probe/v1",
|
||||||
|
"observations": rows,
|
||||||
|
"grants": grants,
|
||||||
|
"replies": replies,
|
||||||
|
"errors": errors,
|
||||||
|
"model_count": 0,
|
||||||
|
"synthetic_source_and_resume": True,
|
||||||
|
"real_time_qualified": False,
|
||||||
|
"one_way_frame_age_measured": False,
|
||||||
|
"actuation_allowed": False,
|
||||||
|
"conditional_relative_rate_ppm": 500,
|
||||||
|
"timestamp_error_budget_ns": "50000",
|
||||||
|
"maximum_mapping_age_ns": "2000000000",
|
||||||
|
"admission_uncertainty_ns": "5000000",
|
||||||
|
"path": "TLS gRPC over existing Mac-Worker authenticated SSH/Tailscale route",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def poll(phase):
|
||||||
|
for _ in range(4):
|
||||||
|
try:
|
||||||
|
probe, access = await control.poll()
|
||||||
|
break
|
||||||
|
except grpc.aio.AioRpcError as exc:
|
||||||
|
errors.append({"phase": phase, "code": exc.code().name})
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("bounded control polling failed")
|
||||||
|
bounds = mapping.add(probe)
|
||||||
|
now = time.monotonic_ns()
|
||||||
|
uncertainty = bounds.uncertainty_ns(now)
|
||||||
|
raw = {k: str(v) if type(v) is int else v for k, v in asdict(probe).items()}
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"phase": phase,
|
||||||
|
"probe": raw,
|
||||||
|
"bounds": bounds.to_dict(),
|
||||||
|
"evaluated_at_ns": str(now),
|
||||||
|
"uncertainty_ns": str(uncertainty),
|
||||||
|
"admitted_5ms": uncertainty <= 5_000_000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return access
|
||||||
|
|
||||||
|
try:
|
||||||
|
previous = None
|
||||||
|
for phase in (1, 2):
|
||||||
|
if phase == 2:
|
||||||
|
await asyncio.sleep(2.3)
|
||||||
|
try:
|
||||||
|
mapping.current(time.monotonic_ns())
|
||||||
|
except ClockMappingError:
|
||||||
|
report["mapping_expired_during_gap"] = True
|
||||||
|
else:
|
||||||
|
raise AssertionError("old clock mapping survived expiry")
|
||||||
|
access = None
|
||||||
|
for _ in range(32):
|
||||||
|
candidate = await poll(phase)
|
||||||
|
if candidate is not None:
|
||||||
|
access = candidate
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
assert access is not None and (previous is None or access.epoch != previous.epoch)
|
||||||
|
assert previous is None or access.token != previous.token
|
||||||
|
grants.append(
|
||||||
|
{
|
||||||
|
"phase": phase,
|
||||||
|
"epoch": access.epoch.to_dict(),
|
||||||
|
"token_changed": previous is not None,
|
||||||
|
"delivery": "authenticated-control-poll",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stream = GrpcStreamClient(target, (root / "cert.pem").read_bytes(), access)
|
||||||
|
try:
|
||||||
|
await stream.open()
|
||||||
|
for sequence in range((phase - 1) * 4 + 1, phase * 4 + 1):
|
||||||
|
payload = bytes([sequence]) * 32768
|
||||||
|
event = LiveIngressEvent(
|
||||||
|
sequence,
|
||||||
|
"synthetic-capture",
|
||||||
|
1,
|
||||||
|
"lidar",
|
||||||
|
"lidar",
|
||||||
|
sequence,
|
||||||
|
1_799_999_999_123_456_789,
|
||||||
|
1_000_000_000,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
started = time.monotonic_ns()
|
||||||
|
await stream.send(event)
|
||||||
|
result = await stream.receive()
|
||||||
|
elapsed = time.monotonic_ns() - started
|
||||||
|
returned = json.loads(result[1])
|
||||||
|
assert (
|
||||||
|
result[0] == sequence and returned["sha256"] == sha256(payload).hexdigest()
|
||||||
|
)
|
||||||
|
replies.append({**returned, "round_trip_ns": str(elapsed)})
|
||||||
|
assert await poll(phase) is None # Control still works during Exchange.
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
if phase == 2:
|
||||||
|
await stream.end()
|
||||||
|
assert await stream.receive() is None
|
||||||
|
finally:
|
||||||
|
await stream.close()
|
||||||
|
previous = access
|
||||||
|
report["passed"] = True
|
||||||
|
finally:
|
||||||
|
await control.close()
|
||||||
|
report["finished_monotonic_ns"] = str(time.monotonic_ns())
|
||||||
|
document(root / "client-report.json", report)
|
||||||
|
bootstrap.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("mode", choices=("server", "client"))
|
||||||
|
parser.add_argument("root", type=Path)
|
||||||
|
parser.add_argument("--target", default="localhost:18561")
|
||||||
|
args = parser.parse_args()
|
||||||
|
os.umask(0o077)
|
||||||
|
asyncio.run(server(args.root) if args.mode == "server" else client(args.root, args.target))
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
"""Bounded, conditional mapping of two process-scoped monotonic clocks.
|
||||||
|
|
||||||
|
Four timestamps bound remote-minus-local offset WITHOUT symmetric-path claims.
|
||||||
|
Intersect recent intervals, widened for an explicit relative-rate/error budget.
|
||||||
|
Neither OS clock is adjusted. The rate/error envelope is an assumption supplied
|
||||||
|
by the controller, not a hardware accuracy certificate inferred from low RTT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .streaming_wire import identifier
|
||||||
|
|
||||||
|
|
||||||
|
class ClockMappingError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _stamp(value: int) -> None:
|
||||||
|
if type(value) is not int or not 0 <= value < 2**63:
|
||||||
|
raise ClockMappingError("invalid monotonic timestamp")
|
||||||
|
|
||||||
|
|
||||||
|
def _drift(duration_ns: int, ppm: int) -> int:
|
||||||
|
return (duration_ns * ppm + 999_999) // 1_000_000
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ClockProbe:
|
||||||
|
local_clock_id: str
|
||||||
|
remote_clock_id: str
|
||||||
|
nonce: str
|
||||||
|
local_send_ns: int
|
||||||
|
remote_receive_ns: int
|
||||||
|
remote_send_ns: int
|
||||||
|
local_receive_ns: int
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
for value in (self.local_clock_id, self.remote_clock_id, self.nonce):
|
||||||
|
identifier(value)
|
||||||
|
for stamp in (
|
||||||
|
self.local_send_ns,
|
||||||
|
self.remote_receive_ns,
|
||||||
|
self.remote_send_ns,
|
||||||
|
self.local_receive_ns,
|
||||||
|
):
|
||||||
|
_stamp(stamp)
|
||||||
|
if (
|
||||||
|
not 0 <= self.local_receive_ns - self.local_send_ns <= 2_000_000_000
|
||||||
|
or self.remote_send_ns < self.remote_receive_ns
|
||||||
|
):
|
||||||
|
raise ClockMappingError("clock probe ordering/deadline violated")
|
||||||
|
|
||||||
|
def bounds_at(
|
||||||
|
self, local_ns: int, *, rate_ppm: int, timestamp_error_ns: int
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
_stamp(local_ns)
|
||||||
|
if local_ns < self.local_receive_ns:
|
||||||
|
raise ClockMappingError("clock observation is from the future")
|
||||||
|
# Any outbound/inbound asymmetry is inside these bounds. Remote receive
|
||||||
|
# and send occur between local send and receive; widen across the whole
|
||||||
|
# interval, not just elapsed time after receipt.
|
||||||
|
widen = _drift(local_ns - self.local_send_ns, rate_ppm) + 2 * timestamp_error_ns
|
||||||
|
return (
|
||||||
|
self.remote_send_ns - self.local_receive_ns - widen,
|
||||||
|
self.remote_receive_ns - self.local_send_ns + widen,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ClockBounds:
|
||||||
|
local_clock_id: str
|
||||||
|
remote_clock_id: str
|
||||||
|
measured_at_ns: int
|
||||||
|
expires_at_ns: int
|
||||||
|
offset_lower_ns: int
|
||||||
|
offset_upper_ns: int
|
||||||
|
rate_ppm: int
|
||||||
|
timestamp_error_ns: int
|
||||||
|
samples: int
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
for value in (self.local_clock_id, self.remote_clock_id):
|
||||||
|
identifier(value)
|
||||||
|
_stamp(self.measured_at_ns)
|
||||||
|
_stamp(self.expires_at_ns)
|
||||||
|
if (
|
||||||
|
self.measured_at_ns >= self.expires_at_ns
|
||||||
|
or type(self.offset_lower_ns) is not int
|
||||||
|
or type(self.offset_upper_ns) is not int
|
||||||
|
or self.offset_lower_ns > self.offset_upper_ns
|
||||||
|
or type(self.rate_ppm) is not int
|
||||||
|
or not 1 <= self.rate_ppm <= 1000
|
||||||
|
or type(self.timestamp_error_ns) is not int
|
||||||
|
or not 1 <= self.timestamp_error_ns <= 1_000_000
|
||||||
|
or type(self.samples) is not int
|
||||||
|
or not 1 <= self.samples <= 16
|
||||||
|
):
|
||||||
|
raise ClockMappingError("invalid clock bounds")
|
||||||
|
|
||||||
|
def offset_at(self, local_ns: int) -> tuple[int, int]:
|
||||||
|
_stamp(local_ns)
|
||||||
|
if not self.measured_at_ns <= local_ns < self.expires_at_ns:
|
||||||
|
raise ClockMappingError("clock mapping expired or local clock regressed")
|
||||||
|
extra = _drift(local_ns - self.measured_at_ns, self.rate_ppm)
|
||||||
|
return self.offset_lower_ns - extra, self.offset_upper_ns + extra
|
||||||
|
|
||||||
|
def uncertainty_ns(self, local_ns: int) -> int:
|
||||||
|
lower, upper = self.offset_at(local_ns)
|
||||||
|
return (upper - lower + 1) // 2
|
||||||
|
|
||||||
|
def require(self, local_ns: int, maximum_uncertainty_ns: int = 5_000_000) -> None:
|
||||||
|
if type(maximum_uncertainty_ns) is not int or maximum_uncertainty_ns <= 0:
|
||||||
|
raise ClockMappingError("invalid uncertainty gate")
|
||||||
|
if self.uncertainty_ns(local_ns) > maximum_uncertainty_ns:
|
||||||
|
raise ClockMappingError("clock uncertainty exceeds admission budget")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": "missioncore.monotonic-clock-bounds/v1",
|
||||||
|
"local_clock_id": self.local_clock_id,
|
||||||
|
"remote_clock_id": self.remote_clock_id,
|
||||||
|
"measured_at_ns": str(self.measured_at_ns),
|
||||||
|
"expires_at_ns": str(self.expires_at_ns),
|
||||||
|
"offset_lower_ns": str(self.offset_lower_ns),
|
||||||
|
"offset_upper_ns": str(self.offset_upper_ns),
|
||||||
|
"relative_rate_budget_ppm": self.rate_ppm,
|
||||||
|
"timestamp_error_budget_ns": str(self.timestamp_error_ns),
|
||||||
|
"samples": self.samples,
|
||||||
|
"conditional_rate_error_envelope": True,
|
||||||
|
"symmetric_network_assumed": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ClockWindow:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
local_clock_id: str,
|
||||||
|
remote_clock_id: str,
|
||||||
|
*,
|
||||||
|
rate_ppm: int,
|
||||||
|
timestamp_error_ns: int,
|
||||||
|
maximum_age_ns: int = 2_000_000_000,
|
||||||
|
) -> None:
|
||||||
|
for value in (local_clock_id, remote_clock_id):
|
||||||
|
identifier(value)
|
||||||
|
if (
|
||||||
|
type(rate_ppm) is not int
|
||||||
|
or not 1 <= rate_ppm <= 1000
|
||||||
|
or type(timestamp_error_ns) is not int
|
||||||
|
or not 1 <= timestamp_error_ns <= 1_000_000
|
||||||
|
or type(maximum_age_ns) is not int
|
||||||
|
or not 0 < maximum_age_ns <= 5_000_000_000
|
||||||
|
):
|
||||||
|
raise ClockMappingError("explicit bounded clock envelope is required")
|
||||||
|
self.local_clock_id, self.remote_clock_id = local_clock_id, remote_clock_id
|
||||||
|
self.rate_ppm, self.timestamp_error_ns, self.maximum_age_ns = (
|
||||||
|
rate_ppm,
|
||||||
|
timestamp_error_ns,
|
||||||
|
maximum_age_ns,
|
||||||
|
)
|
||||||
|
self.samples: deque[ClockProbe] = deque(maxlen=16)
|
||||||
|
self.failed = False
|
||||||
|
self.last_receive_ns = -1
|
||||||
|
|
||||||
|
def add(self, sample: ClockProbe) -> ClockBounds:
|
||||||
|
if self.failed:
|
||||||
|
raise ClockMappingError("clock session quarantined; explicit new session required")
|
||||||
|
if (
|
||||||
|
(sample.local_clock_id, sample.remote_clock_id)
|
||||||
|
!= (self.local_clock_id, self.remote_clock_id)
|
||||||
|
or sample.local_send_ns <= self.last_receive_ns
|
||||||
|
or any(old.nonce == sample.nonce for old in self.samples)
|
||||||
|
):
|
||||||
|
raise ClockMappingError("foreign, overlapping or replayed clock probe")
|
||||||
|
while (
|
||||||
|
self.samples
|
||||||
|
and sample.local_receive_ns - self.samples[0].local_receive_ns >= self.maximum_age_ns
|
||||||
|
):
|
||||||
|
self.samples.popleft()
|
||||||
|
# A new sample must intersect the still-valid old evidence. Do not
|
||||||
|
# silently forget a contradictory sample to manufacture a narrower fit.
|
||||||
|
bounds = [
|
||||||
|
old.bounds_at(
|
||||||
|
sample.local_receive_ns,
|
||||||
|
rate_ppm=self.rate_ppm,
|
||||||
|
timestamp_error_ns=self.timestamp_error_ns,
|
||||||
|
)
|
||||||
|
for old in (*self.samples, sample)
|
||||||
|
]
|
||||||
|
lower, upper = max(x[0] for x in bounds), min(x[1] for x in bounds)
|
||||||
|
if lower > upper:
|
||||||
|
self.failed = True
|
||||||
|
raise ClockMappingError("clock jump or violated rate/error envelope")
|
||||||
|
self.samples.append(sample)
|
||||||
|
self.last_receive_ns = sample.local_receive_ns
|
||||||
|
return self.current(sample.local_receive_ns)
|
||||||
|
|
||||||
|
def current(self, local_ns: int) -> ClockBounds:
|
||||||
|
_stamp(local_ns)
|
||||||
|
if self.failed or not self.samples or local_ns < self.last_receive_ns:
|
||||||
|
raise ClockMappingError("no valid monotonic clock mapping")
|
||||||
|
recent = [x for x in self.samples if local_ns - x.local_receive_ns < self.maximum_age_ns]
|
||||||
|
if not recent:
|
||||||
|
raise ClockMappingError("clock mapping expired")
|
||||||
|
bounds = [
|
||||||
|
x.bounds_at(
|
||||||
|
local_ns, rate_ppm=self.rate_ppm, timestamp_error_ns=self.timestamp_error_ns
|
||||||
|
)
|
||||||
|
for x in recent
|
||||||
|
]
|
||||||
|
lower, upper = max(x[0] for x in bounds), min(x[1] for x in bounds)
|
||||||
|
return ClockBounds(
|
||||||
|
self.local_clock_id,
|
||||||
|
self.remote_clock_id,
|
||||||
|
local_ns,
|
||||||
|
min(x.local_receive_ns for x in recent) + self.maximum_age_ns,
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
self.rate_ppm,
|
||||||
|
self.timestamp_error_ns,
|
||||||
|
len(recent),
|
||||||
|
)
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
"""Capability-scoped clock observation and pending-grant delivery over TLS.
|
||||||
|
|
||||||
|
Poll cannot acquire/renew a GPU lease, pick an epoch, reset models or cancel a
|
||||||
|
run. Only the trusted local controller supplies a pending grant. A request nonce
|
||||||
|
binds each clock response; authenticated polling is rate/buffer bounded. The
|
||||||
|
per-activation bootstrap ticket is provisioned separately, never in CLI/logs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import secrets
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass, field, replace
|
||||||
|
from hashlib import sha256
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import grpc # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from . import streaming_wire as wire
|
||||||
|
from .realtime_contract import StreamStart
|
||||||
|
from .streaming_clock import ClockProbe
|
||||||
|
from .streaming_grpc import OPTIONS, StreamAccess
|
||||||
|
|
||||||
|
SERVICE = "missioncore.perception.v1.StreamControl"
|
||||||
|
METHOD = f"/{SERVICE}/Poll"
|
||||||
|
MAX_CONTROL = 8192
|
||||||
|
|
||||||
|
|
||||||
|
def _document(raw: bytes) -> dict[str, Any]:
|
||||||
|
if type(raw) is not bytes or not 0 < len(raw) <= MAX_CONTROL:
|
||||||
|
raise wire.StreamWireError("control message exceeds bound")
|
||||||
|
return wire.parse_header(bytearray(raw))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(value: object) -> None:
|
||||||
|
if (
|
||||||
|
not isinstance(value, str)
|
||||||
|
or len(value) != 64
|
||||||
|
or any(c not in "0123456789abcdef" for c in value)
|
||||||
|
):
|
||||||
|
raise ValueError("invalid capability")
|
||||||
|
|
||||||
|
|
||||||
|
def _offer(access: StreamAccess, activation: StreamStart) -> None:
|
||||||
|
if replace(access.epoch, epoch_id=activation.epoch_id) != activation:
|
||||||
|
raise ValueError("pending grant belongs to another activation")
|
||||||
|
wire.identifier(access.session_id)
|
||||||
|
wire.decimal(access.session_generation)
|
||||||
|
if access.session_generation < 1:
|
||||||
|
raise ValueError("invalid capture generation")
|
||||||
|
_token(access.token)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ControlTicket:
|
||||||
|
activation: StreamStart
|
||||||
|
token: str = field(repr=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_token(self.token)
|
||||||
|
|
||||||
|
def metadata(self) -> tuple[tuple[str, str], ...]:
|
||||||
|
return (
|
||||||
|
("authorization", "Bearer " + self.token),
|
||||||
|
("x-stream-binding", wire.binding(self.activation)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StreamControlEndpoint:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ticket: ControlTicket,
|
||||||
|
pending: Callable[[], StreamAccess | None],
|
||||||
|
*,
|
||||||
|
clock_id: str,
|
||||||
|
clock_ns: Callable[[], int] = time.monotonic_ns,
|
||||||
|
) -> None:
|
||||||
|
wire.identifier(clock_id)
|
||||||
|
self.activation, self.pending, self.clock_id, self.clock_ns = (
|
||||||
|
ticket.activation,
|
||||||
|
pending,
|
||||||
|
clock_id,
|
||||||
|
clock_ns,
|
||||||
|
)
|
||||||
|
self._token_hash = sha256(ticket.token.encode()).digest()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._last_poll_ns = -1
|
||||||
|
self.accepted = self.rejected = 0
|
||||||
|
|
||||||
|
def handler(self) -> Any:
|
||||||
|
return grpc.method_handlers_generic_handler(
|
||||||
|
SERVICE, {"Poll": grpc.unary_unary_rpc_method_handler(self.poll)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _authenticate(self, context: Any) -> None:
|
||||||
|
if b"ssl" not in context.auth_context().get("transport_security_type", ()):
|
||||||
|
raise wire.StreamWireError("TLS control capability required")
|
||||||
|
metadata = list(context.invocation_metadata())
|
||||||
|
relevant = [(k, v) for k, v in metadata if k in ("authorization", "x-stream-binding")]
|
||||||
|
values = dict(relevant)
|
||||||
|
token = values.get("authorization")
|
||||||
|
if (
|
||||||
|
len(relevant) != 2
|
||||||
|
or len(values) != 2
|
||||||
|
or not isinstance(token, str)
|
||||||
|
or len(token) != 71
|
||||||
|
or not token.startswith("Bearer ")
|
||||||
|
or values.get("x-stream-binding") != wire.binding(self.activation)
|
||||||
|
or not hmac.compare_digest(sha256(token[7:].encode()).digest(), self._token_hash)
|
||||||
|
):
|
||||||
|
raise wire.StreamWireError("control capability rejected")
|
||||||
|
|
||||||
|
async def poll(self, raw: bytes, context: Any) -> bytes:
|
||||||
|
received = self.clock_ns()
|
||||||
|
try:
|
||||||
|
self._authenticate(context)
|
||||||
|
request = _document(raw)
|
||||||
|
if set(request) != {"nonce", "local_clock_id", "local_send_ns"}:
|
||||||
|
raise wire.StreamWireError("unsupported control request")
|
||||||
|
wire.identifier(request["nonce"])
|
||||||
|
wire.identifier(request["local_clock_id"])
|
||||||
|
if wire.uint64(request["local_send_ns"]) >= 2**63:
|
||||||
|
raise ValueError("invalid monotonic timestamp")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
self.rejected += 1
|
||||||
|
await context.abort(
|
||||||
|
grpc.StatusCode.UNAUTHENTICATED, "control capability/request rejected"
|
||||||
|
)
|
||||||
|
return b""
|
||||||
|
with self._lock:
|
||||||
|
limited = self._last_poll_ns >= 0 and received - self._last_poll_ns < 25_000_000
|
||||||
|
if not limited:
|
||||||
|
self._last_poll_ns = received
|
||||||
|
if limited:
|
||||||
|
self.rejected += 1
|
||||||
|
await context.abort(
|
||||||
|
grpc.StatusCode.RESOURCE_EXHAUSTED, "bounded control poll rate exceeded"
|
||||||
|
)
|
||||||
|
return b""
|
||||||
|
try:
|
||||||
|
offer = self.pending() # Trusted constant-time snapshot; no model/control work.
|
||||||
|
if offer is not None:
|
||||||
|
_offer(offer, self.activation)
|
||||||
|
response = {
|
||||||
|
"schema_version": "missioncore.stream-control-poll/v1",
|
||||||
|
"activation_binding": wire.binding(self.activation),
|
||||||
|
**request,
|
||||||
|
"remote_clock_id": self.clock_id,
|
||||||
|
"remote_receive_ns": str(received),
|
||||||
|
"remote_send_ns": str(self.clock_ns()),
|
||||||
|
"grant": None
|
||||||
|
if offer is None
|
||||||
|
else {
|
||||||
|
"epoch": offer.epoch.to_dict(),
|
||||||
|
"session_id": offer.session_id,
|
||||||
|
"session_generation": str(offer.session_generation),
|
||||||
|
"token": offer.token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded = wire.canonical(response)
|
||||||
|
if len(encoded) > MAX_CONTROL:
|
||||||
|
raise ValueError("trusted control snapshot exceeds bound")
|
||||||
|
except (ValueError, RuntimeError):
|
||||||
|
await context.abort(
|
||||||
|
grpc.StatusCode.UNAVAILABLE, "local controller has no current offer"
|
||||||
|
)
|
||||||
|
return b""
|
||||||
|
self.accepted += 1
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
|
class StreamControlClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: str,
|
||||||
|
roots: bytes,
|
||||||
|
ticket: ControlTicket,
|
||||||
|
*,
|
||||||
|
clock_id: str,
|
||||||
|
clock_ns: Callable[[], int] = time.monotonic_ns,
|
||||||
|
) -> None:
|
||||||
|
if not roots:
|
||||||
|
raise ValueError("trusted TLS roots required")
|
||||||
|
wire.identifier(clock_id)
|
||||||
|
self.ticket, self.clock_id, self.clock_ns = ticket, clock_id, clock_ns
|
||||||
|
self.channel = grpc.aio.secure_channel(
|
||||||
|
target,
|
||||||
|
grpc.ssl_channel_credentials(root_certificates=roots),
|
||||||
|
options=OPTIONS
|
||||||
|
+ (
|
||||||
|
("grpc.max_send_message_length", MAX_CONTROL),
|
||||||
|
("grpc.max_receive_message_length", MAX_CONTROL),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.call = self.channel.unary_unary(METHOD)
|
||||||
|
self.polling = False
|
||||||
|
|
||||||
|
async def poll(self) -> tuple[ClockProbe, StreamAccess | None]:
|
||||||
|
if self.polling:
|
||||||
|
raise ValueError("one outstanding clock/control probe per client")
|
||||||
|
self.polling = True
|
||||||
|
nonce = secrets.token_hex(16)
|
||||||
|
sent = self.clock_ns()
|
||||||
|
try:
|
||||||
|
raw = await self.call(
|
||||||
|
wire.canonical(
|
||||||
|
{"nonce": nonce, "local_clock_id": self.clock_id, "local_send_ns": str(sent)}
|
||||||
|
),
|
||||||
|
metadata=self.ticket.metadata(),
|
||||||
|
timeout=0.5,
|
||||||
|
wait_for_ready=False,
|
||||||
|
)
|
||||||
|
received = self.clock_ns()
|
||||||
|
response = _document(raw)
|
||||||
|
if (
|
||||||
|
set(response)
|
||||||
|
!= {
|
||||||
|
"schema_version",
|
||||||
|
"activation_binding",
|
||||||
|
"nonce",
|
||||||
|
"local_clock_id",
|
||||||
|
"local_send_ns",
|
||||||
|
"remote_clock_id",
|
||||||
|
"remote_receive_ns",
|
||||||
|
"remote_send_ns",
|
||||||
|
"grant",
|
||||||
|
}
|
||||||
|
or response["schema_version"] != "missioncore.stream-control-poll/v1"
|
||||||
|
or response["activation_binding"] != wire.binding(self.ticket.activation)
|
||||||
|
or response["nonce"] != nonce
|
||||||
|
or response["local_clock_id"] != self.clock_id
|
||||||
|
or response["local_send_ns"] != str(sent)
|
||||||
|
):
|
||||||
|
raise wire.StreamWireError("clock/control response binding mismatch")
|
||||||
|
probe = ClockProbe(
|
||||||
|
self.clock_id,
|
||||||
|
response["remote_clock_id"],
|
||||||
|
nonce,
|
||||||
|
sent,
|
||||||
|
wire.uint64(response["remote_receive_ns"]),
|
||||||
|
wire.uint64(response["remote_send_ns"]),
|
||||||
|
received,
|
||||||
|
)
|
||||||
|
grant = response["grant"]
|
||||||
|
access = None
|
||||||
|
if grant is not None:
|
||||||
|
if not isinstance(grant, dict) or set(grant) != {
|
||||||
|
"epoch",
|
||||||
|
"session_id",
|
||||||
|
"session_generation",
|
||||||
|
"token",
|
||||||
|
}:
|
||||||
|
raise wire.StreamWireError("invalid control grant shape")
|
||||||
|
access = StreamAccess(
|
||||||
|
StreamStart.from_dict(grant["epoch"]),
|
||||||
|
grant["session_id"],
|
||||||
|
wire.uint64(grant["session_generation"]),
|
||||||
|
grant["token"],
|
||||||
|
)
|
||||||
|
_offer(access, self.ticket.activation)
|
||||||
|
return probe, access
|
||||||
|
finally:
|
||||||
|
self.polling = False
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
await self.channel.close()
|
||||||
@@ -149,7 +149,14 @@ class GrpcStreamEndpoint:
|
|||||||
if self.active is not None and self.active[0] == epoch:
|
if self.active is not None and self.active[0] == epoch:
|
||||||
self.active[1].finish()
|
self.active[1].finish()
|
||||||
|
|
||||||
async def serve(self, address: str, *, certificate: bytes, private_key: bytes) -> Any:
|
async def serve(
|
||||||
|
self,
|
||||||
|
address: str,
|
||||||
|
*,
|
||||||
|
certificate: bytes,
|
||||||
|
private_key: bytes,
|
||||||
|
control_handlers: tuple[Any, ...] = (),
|
||||||
|
) -> Any:
|
||||||
if not certificate or not private_key:
|
if not certificate or not private_key:
|
||||||
raise ValueError("TLS identity is required")
|
raise ValueError("TLS identity is required")
|
||||||
server = grpc.aio.server(
|
server = grpc.aio.server(
|
||||||
@@ -158,7 +165,11 @@ class GrpcStreamEndpoint:
|
|||||||
("grpc.max_receive_message_length", INPUT_CHUNK),
|
("grpc.max_receive_message_length", INPUT_CHUNK),
|
||||||
("grpc.max_send_message_length", MAX_REPLY),
|
("grpc.max_send_message_length", MAX_REPLY),
|
||||||
),
|
),
|
||||||
maximum_concurrent_rpcs=2,
|
# aio reserves its next accept before awaiting a request. Leave
|
||||||
|
# one bounded accept slot beside Exchange + a completing Poll;
|
||||||
|
# otherwise the next Poll can inherit a stale "limit exceeded".
|
||||||
|
# Data ownership still admits exactly one Exchange, never 2 GPUs.
|
||||||
|
maximum_concurrent_rpcs=3 if control_handlers else 2,
|
||||||
compression=grpc.Compression.NoCompression,
|
compression=grpc.Compression.NoCompression,
|
||||||
)
|
)
|
||||||
server.add_generic_rpc_handlers(
|
server.add_generic_rpc_handlers(
|
||||||
@@ -166,6 +177,7 @@ class GrpcStreamEndpoint:
|
|||||||
grpc.method_handlers_generic_handler(
|
grpc.method_handlers_generic_handler(
|
||||||
SERVICE, {"Exchange": grpc.stream_stream_rpc_method_handler(self.exchange)}
|
SERVICE, {"Exchange": grpc.stream_stream_rpc_method_handler(self.exchange)}
|
||||||
),
|
),
|
||||||
|
*control_handlers,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
port = server.add_secure_port(
|
port = server.add_secure_port(
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Conditional clock intervals, never a symmetric-network offset assertion."""
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from k1link.perception.streaming_clock import ClockMappingError, ClockProbe, ClockWindow
|
||||||
|
|
||||||
|
BASE = 10**16 # Deliberately above IEEE754 exact-integer range.
|
||||||
|
|
||||||
|
|
||||||
|
def probe(number=1, *, outbound=1_000_000, inbound=2_000_000, offset=-(10**12)):
|
||||||
|
sent = BASE + number * 100_000_000
|
||||||
|
return ClockProbe(
|
||||||
|
"mac",
|
||||||
|
"worker",
|
||||||
|
str(number),
|
||||||
|
sent,
|
||||||
|
sent + offset + outbound,
|
||||||
|
sent + offset + outbound + 100_000,
|
||||||
|
sent + outbound + inbound + 100_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def window(**kwargs):
|
||||||
|
return ClockWindow("mac", "worker", rate_ppm=500, timestamp_error_ns=50_000, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("offset", [-(10**12), 0, 10**12])
|
||||||
|
@pytest.mark.parametrize("outbound,inbound", [(1, 9_000_000), (9_000_000, 1), (1, 1)])
|
||||||
|
def test_contains_true_offset_without_symmetric_path_assumption(offset, outbound, inbound):
|
||||||
|
sample = probe(offset=offset, outbound=outbound, inbound=inbound)
|
||||||
|
mapping = window().add(sample)
|
||||||
|
now = sample.local_receive_ns
|
||||||
|
assert mapping.offset_at(now)[0] <= offset <= mapping.offset_at(now)[1]
|
||||||
|
assert mapping.uncertainty_ns(now + 1_000_000) > mapping.uncertainty_ns(now)
|
||||||
|
value = mapping.to_dict()
|
||||||
|
assert value["measured_at_ns"] == str(now)
|
||||||
|
assert not value["symmetric_network_assumed"]
|
||||||
|
assert value["timestamp_error_budget_ns"] == "50000"
|
||||||
|
|
||||||
|
|
||||||
|
def test_intersection_requires_evidence_and_drift_is_not_averaged_away():
|
||||||
|
mapping = window()
|
||||||
|
first = mapping.add(probe(outbound=1_000_000, inbound=20_000_000))
|
||||||
|
with pytest.raises(ClockMappingError, match="budget"):
|
||||||
|
first.require(first.measured_at_ns)
|
||||||
|
second = mapping.add(probe(2, outbound=20_000_000, inbound=1_000_000))
|
||||||
|
second.require(second.measured_at_ns)
|
||||||
|
assert second.uncertainty_ns(second.measured_at_ns) < 2_000_000
|
||||||
|
assert (
|
||||||
|
second.offset_at(second.measured_at_ns)[0]
|
||||||
|
< -(10**12)
|
||||||
|
< second.offset_at(second.measured_at_ns)[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_expired_mapping_waits_and_new_observation_can_restore_it():
|
||||||
|
mapping = window()
|
||||||
|
first = mapping.add(probe())
|
||||||
|
with pytest.raises(ClockMappingError, match="expired"):
|
||||||
|
first.require(first.expires_at_ns)
|
||||||
|
with pytest.raises(ClockMappingError, match="expired"):
|
||||||
|
mapping.current(first.expires_at_ns)
|
||||||
|
resumed = mapping.add(probe(30))
|
||||||
|
assert resumed.samples == 1
|
||||||
|
resumed.require(resumed.measured_at_ns)
|
||||||
|
|
||||||
|
|
||||||
|
def test_contradictory_samples_quarantine_not_cherry_pick():
|
||||||
|
mapping = window()
|
||||||
|
mapping.add(probe())
|
||||||
|
with pytest.raises(ClockMappingError, match="envelope"):
|
||||||
|
mapping.add(probe(2, offset=0))
|
||||||
|
with pytest.raises(ClockMappingError, match="quarantined"):
|
||||||
|
mapping.add(probe(3))
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
mapping.current(BASE + 3_000_000_000)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fault", ["local", "remote", "nonce", "overlap"])
|
||||||
|
def test_foreign_or_replayed_observation_cannot_refresh_mapping(fault):
|
||||||
|
mapping = window()
|
||||||
|
original = probe()
|
||||||
|
mapping.add(original)
|
||||||
|
second = probe(2)
|
||||||
|
changes = {
|
||||||
|
"local": {"local_clock_id": "other"},
|
||||||
|
"remote": {"remote_clock_id": "rebooted"},
|
||||||
|
"nonce": {"nonce": original.nonce},
|
||||||
|
"overlap": {"local_send_ns": original.local_receive_ns},
|
||||||
|
}
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
mapping.add(replace(second, **changes[fault]))
|
||||||
|
assert mapping.last_receive_ns == original.local_receive_ns
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_is_bounded_and_exact_values_survive():
|
||||||
|
mapping = window(maximum_age_ns=5_000_000_000)
|
||||||
|
for number in range(1, 35):
|
||||||
|
bounds = mapping.add(probe(number))
|
||||||
|
assert len(mapping.samples) == bounds.samples == 16
|
||||||
|
assert bounds.to_dict()["measured_at_ns"] == str(probe(34).local_receive_ns)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [-1, 2**63, True, 1.0])
|
||||||
|
def test_invalid_monotonic_timestamp_rejected(value):
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
replace(probe(), local_send_ns=value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deadline_remote_order_and_backward_reads():
|
||||||
|
sample = probe()
|
||||||
|
for changes in (
|
||||||
|
{"local_receive_ns": sample.local_send_ns - 1},
|
||||||
|
{"local_receive_ns": sample.local_send_ns + 2_000_000_001},
|
||||||
|
{"remote_send_ns": sample.remote_receive_ns - 1},
|
||||||
|
):
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
replace(sample, **changes)
|
||||||
|
mapping = window().add(sample)
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
mapping.offset_at(mapping.measured_at_ns - 1)
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
replace(mapping, offset_lower_ns=mapping.offset_upper_ns + 1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"rate,error,age",
|
||||||
|
[(0, 1, 1), (1001, 1, 1), (True, 1, 1), (1, 0, 1), (1, 1_000_001, 1), (1, 1, 0)],
|
||||||
|
)
|
||||||
|
def test_controller_must_supply_bounded_envelope(rate, error, age):
|
||||||
|
with pytest.raises(ClockMappingError):
|
||||||
|
ClockWindow("mac", "worker", rate_ppm=rate, timestamp_error_ns=error, maximum_age_ns=age)
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"""Small TLS clock/grant checks; no model workload or lease authority in Poll."""
|
||||||
|
|
||||||
|
# ruff: noqa: E402, F811 -- imported shared pytest TLS fixture.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from dataclasses import replace
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
grpc = pytest.importorskip("grpc")
|
||||||
|
|
||||||
|
from test_perception_streaming_grpc import event, eventually, identity, tls # noqa: F401
|
||||||
|
|
||||||
|
from k1link.perception import streaming_wire as wire
|
||||||
|
from k1link.perception.streaming_clock import ClockWindow
|
||||||
|
from k1link.perception.streaming_continuity import ResumeEvidence
|
||||||
|
from k1link.perception.streaming_control_grpc import (
|
||||||
|
MAX_CONTROL,
|
||||||
|
ControlTicket,
|
||||||
|
StreamControlClient,
|
||||||
|
StreamControlEndpoint,
|
||||||
|
_offer,
|
||||||
|
)
|
||||||
|
from k1link.perception.streaming_grpc import GrpcStreamClient, GrpcStreamEndpoint, StreamAccess
|
||||||
|
from k1link.perception.streaming_lifecycle import StreamingLifecycle
|
||||||
|
from k1link.perception.streaming_queue import StreamMailbox
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def controlled(tmp_path, tls):
|
||||||
|
run = StreamingLifecycle(
|
||||||
|
identity(),
|
||||||
|
tmp_path,
|
||||||
|
StreamMailbox(),
|
||||||
|
threading.Event(),
|
||||||
|
recover_input=True,
|
||||||
|
source_clock_ns=lambda: 1_000_000_000,
|
||||||
|
)
|
||||||
|
run.ready()
|
||||||
|
endpoint = GrpcStreamEndpoint(
|
||||||
|
run,
|
||||||
|
lambda value: endpoint.publish(
|
||||||
|
run.continuity.epoch, value.ingress_sequence, sha256(value.payload).digest()
|
||||||
|
),
|
||||||
|
lambda _: None,
|
||||||
|
)
|
||||||
|
pending = [endpoint.issue(identity(), "capture", 1)]
|
||||||
|
ticket = ControlTicket(identity(), "b" * 64)
|
||||||
|
control = StreamControlEndpoint(ticket, lambda: pending[0], clock_id="worker")
|
||||||
|
server, port = await endpoint.serve(
|
||||||
|
"localhost:0",
|
||||||
|
certificate=tls[0],
|
||||||
|
private_key=tls[1],
|
||||||
|
control_handlers=(control.handler(),),
|
||||||
|
)
|
||||||
|
client = StreamControlClient(f"localhost:{port}", tls[0], ticket, clock_id="mac")
|
||||||
|
try:
|
||||||
|
yield run, endpoint, control, client, pending, port
|
||||||
|
finally:
|
||||||
|
await client.close()
|
||||||
|
await server.stop(0)
|
||||||
|
await eventually(lambda: endpoint.active is None)
|
||||||
|
assert run.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_delivery_coexists_with_stream_without_lease_or_model_authority(tmp_path, tls):
|
||||||
|
async def check():
|
||||||
|
async with controlled(tmp_path, tls) as (run, endpoint, control, client, pending, port):
|
||||||
|
probe, access = await client.poll()
|
||||||
|
assert access == pending[0]
|
||||||
|
mapping = ClockWindow("mac", "worker", rate_ppm=500, timestamp_error_ns=50_000)
|
||||||
|
assert mapping.add(probe).uncertainty_ns(probe.local_receive_ns) > 0
|
||||||
|
first = access
|
||||||
|
for phase in range(2):
|
||||||
|
stream = GrpcStreamClient(f"localhost:{port}", tls[0], access)
|
||||||
|
try:
|
||||||
|
await stream.open()
|
||||||
|
pending[0] = None
|
||||||
|
await stream.send(event(phase + 1))
|
||||||
|
assert await stream.receive() == (phase + 1, sha256(event().payload).digest())
|
||||||
|
await asyncio.sleep(0.03)
|
||||||
|
_, absent = await client.poll()
|
||||||
|
assert absent is None and endpoint.active is not None
|
||||||
|
finally:
|
||||||
|
await stream.close()
|
||||||
|
await eventually(lambda: endpoint.active is None)
|
||||||
|
assert run.continuity.phase == "waiting"
|
||||||
|
assert run.lease.start == identity()
|
||||||
|
if phase == 0:
|
||||||
|
run.renew(identity()) # Only independent trusted owner renews lease.
|
||||||
|
epoch = run.begin_input(identity())
|
||||||
|
run.resume_input(
|
||||||
|
epoch, ResumeEvidence(*([1_000_000_000] * 4), True), lambda: None
|
||||||
|
)
|
||||||
|
pending[0] = endpoint.issue(epoch, "capture", 1)
|
||||||
|
await asyncio.sleep(0.03)
|
||||||
|
_, access = await client.poll()
|
||||||
|
assert access.epoch != first.epoch and access.token != first.token
|
||||||
|
assert control.accepted == 4 and run.lease.start.lease_generation == 1
|
||||||
|
|
||||||
|
asyncio.run(check())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fault", ["secret", "binding", "duplicate", "oversize", "int64", "extra"])
|
||||||
|
def test_bad_control_cannot_claim_data_or_change_owner(tmp_path, tls, fault):
|
||||||
|
async def check():
|
||||||
|
async with controlled(tmp_path, tls) as (run, endpoint, control, client, pending, _):
|
||||||
|
metadata = client.ticket.metadata()
|
||||||
|
request = {"nonce": "n", "local_clock_id": "mac", "local_send_ns": "1"}
|
||||||
|
if fault == "secret":
|
||||||
|
metadata = ControlTicket(identity(), "0" * 64).metadata()
|
||||||
|
elif fault == "binding":
|
||||||
|
metadata = ControlTicket(
|
||||||
|
replace(identity(), source_id="other"), "b" * 64
|
||||||
|
).metadata()
|
||||||
|
elif fault == "duplicate":
|
||||||
|
metadata = (*metadata, *metadata)
|
||||||
|
elif fault == "int64":
|
||||||
|
request["local_send_ns"] = str(2**63)
|
||||||
|
elif fault == "extra":
|
||||||
|
request["renew_lease"] = True
|
||||||
|
raw = b"x" * (MAX_CONTROL + 1) if fault == "oversize" else wire.canonical(request)
|
||||||
|
with pytest.raises(grpc.aio.AioRpcError):
|
||||||
|
await client.call(raw, metadata=metadata, timeout=1)
|
||||||
|
assert endpoint.active is None and endpoint.grant is not None
|
||||||
|
assert run.lease.start == identity() and run.mailbox.bytes == 0
|
||||||
|
assert control.accepted == 0
|
||||||
|
|
||||||
|
asyncio.run(check())
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_rate_and_single_outstanding_are_bounded(tmp_path, tls):
|
||||||
|
async def check():
|
||||||
|
async with controlled(tmp_path, tls) as (_, _, control, client, _, _):
|
||||||
|
control.clock_ns = lambda: 10**16
|
||||||
|
await client.poll()
|
||||||
|
with pytest.raises(grpc.aio.AioRpcError) as exc:
|
||||||
|
await client.poll()
|
||||||
|
assert exc.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
|
||||||
|
client.polling = True
|
||||||
|
with pytest.raises(ValueError, match="outstanding"):
|
||||||
|
await client.poll()
|
||||||
|
assert control.accepted == control.rejected == 1
|
||||||
|
|
||||||
|
asyncio.run(check())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"changes",
|
||||||
|
[
|
||||||
|
{"source_id": "other"},
|
||||||
|
{"clock_domain_id": "other"},
|
||||||
|
{"input_mode": "recorded-source-paced"},
|
||||||
|
{"lease_generation": 2},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_offer_is_bound_to_every_activation_field(changes):
|
||||||
|
access = StreamAccess(replace(identity(), **changes), "capture", 1, "a" * 64)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_offer(access, identity())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [None, 1, "x" * 64, "a" * 63])
|
||||||
|
def test_invalid_capabilities_fail_closed_without_leaking_in_repr(value):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ControlTicket(identity(), value)
|
||||||
|
assert "b" * 64 not in repr(ControlTicket(identity(), "b" * 64))
|
||||||
|
|
||||||
|
|
||||||
|
def test_malicious_response_nonce_or_offer_rejected(tls):
|
||||||
|
async def check():
|
||||||
|
ticket = ControlTicket(identity(), "a" * 64)
|
||||||
|
client = StreamControlClient("localhost:1", tls[0], ticket, clock_id="mac")
|
||||||
|
for mode in ("nonce", "offer"):
|
||||||
|
|
||||||
|
async def fake(raw, mode=mode, **kwargs):
|
||||||
|
request = wire.parse_header(bytearray(raw))
|
||||||
|
return wire.canonical(
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.stream-control-poll/v1",
|
||||||
|
"activation_binding": wire.binding(identity()),
|
||||||
|
**request,
|
||||||
|
"nonce": "wrong" if mode == "nonce" else request["nonce"],
|
||||||
|
"remote_clock_id": "worker",
|
||||||
|
"remote_receive_ns": str(time.monotonic_ns()),
|
||||||
|
"remote_send_ns": str(time.monotonic_ns()),
|
||||||
|
"grant": {
|
||||||
|
"epoch": replace(identity(), source_id="foreign").to_dict(),
|
||||||
|
"session_id": "capture",
|
||||||
|
"session_generation": "1",
|
||||||
|
"token": "a" * 64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.call = fake
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await client.poll()
|
||||||
|
assert not client.polling
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
asyncio.run(check())
|
||||||
Reference in New Issue
Block a user