Compare commits

..
Author SHA1 Message Date
QiuSW 82afce1f81 feat: 实现 Sense Brain 控制连接器 (#152) 2026-08-31 11:51:58 +08:00
ila cf00d73436 Merge pull request '#163' from docs/151-machine-identity-acceptance
docs: 同步 #151 机器身份与安全传输 v1
2026-08-31 11:24:10 +08:00
QiuSW ae9bd015c2 docs: 记录三项目机器身份与安全传输 v1 (#151) 2026-08-31 11:23:52 +08:00
ila 06e0790f00 Merge pull request '#162' from task/151-machine-identity
feat: 建立三项目机器身份与安全传输 v1 (#151)
2026-08-31 11:14:52 +08:00
22 changed files with 1646 additions and 10 deletions
@@ -0,0 +1,7 @@
"""Credential-free Sense control-plane connector for Brain."""
from .consumer import ApplyResult, SourceConfigConsumer, SourceConfigError
from .replay import SQLiteReplayStore
from .status import RuntimeStatusPublisher
__all__ = ["ApplyResult", "SourceConfigConsumer", "SourceConfigError", "SQLiteReplayStore", "RuntimeStatusPublisher"]
@@ -0,0 +1,57 @@
"""Machine-authenticated adapters with bounded timeout/backoff and a kill switch."""
from __future__ import annotations
import time
import re
from dataclasses import dataclass
from typing import Callable
from yovision_brain.integration.machine_identity.token import Signer, Verifier, bearer_token
from .consumer import ApplyResult, SourceConfigConsumer
_REQUEST_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{15,127}$")
@dataclass(frozen=True, slots=True)
class ConnectorResponse:
status: int
body: bytes
correlation_id: str
class SourceConfigEndpoint:
def __init__(self, consumer: SourceConfigConsumer, verifier: Verifier, *, enabled: bool = True, max_body_bytes: int = 10 * 1024 * 1024) -> None:
self._consumer, self._verifier, self._enabled, self._max = consumer, verifier, enabled, max_body_bytes
def receive(self, authorization: str, body: bytes, correlation_id: str) -> ApplyResult:
if not self._enabled: raise RuntimeError("CONNECTOR_DISABLED")
if not _REQUEST_ID.fullmatch(correlation_id): raise ValueError("INVALID_CORRELATION_ID")
if len(body) > self._max: raise ValueError("REQUEST_TOO_LARGE")
token=bearer_token(authorization)
self._verifier.verify(token,"yovision-brain","source-config:write","POST","/machine/v1/source-config",body)
return self._consumer.apply(body)
class StatusSender:
def __init__(self, signer: Signer, send: Callable[[str, bytes, str, float], ConnectorResponse], *, enabled: bool = True, timeout_seconds: float = 5.0, max_attempts: int = 4, sleeper: Callable[[float], None] = time.sleep) -> None:
if timeout_seconds <= 0 or max_attempts < 1: raise ValueError("invalid connector retry policy")
self._signer,self._send,self._enabled,self._timeout,self._attempts,self._sleep=signer,send,enabled,timeout_seconds,max_attempts,sleeper
def publish(self, body: bytes, correlation_id: str) -> ConnectorResponse:
if not self._enabled: raise RuntimeError("CONNECTOR_DISABLED")
if not _REQUEST_ID.fullmatch(correlation_id): raise ValueError("INVALID_CORRELATION_ID")
last: Exception|None=None
for attempt in range(self._attempts):
try:
# A retry gets a fresh jti: the previous request may have been
# accepted even when its response was lost.
token=self._signer.mint("yovision-sense",("runtime-status:write",),"POST","/machine/v1/runtime-status",body)
response=self._send("Bearer "+token,body,correlation_id,self._timeout)
if 200<=response.status<300:return response
if response.status<500:raise RuntimeError(f"STATUS_REJECTED_{response.status}")
last=RuntimeError(f"STATUS_REMOTE_{response.status}")
except (TimeoutError,ConnectionError) as exc:last=exc
if attempt+1<self._attempts:self._sleep(min(2**attempt,30))
raise RuntimeError("STATUS_DELIVERY_EXHAUSTED") from last
@@ -0,0 +1,250 @@
"""Strict source-config/v1 validation and atomic last-known-good application."""
from __future__ import annotations
import hashlib
import hmac
import json
import re
import sqlite3
import threading
import time
from contextlib import closing
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Mapping
from urllib.parse import urlsplit
from yovision_brain.rules.models import AreaDefinition, DirectionalLineDefinition, NormalizedPoint, RuleSet
VERSION = "yovision.source-config/v1"
_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$")
_EXTENSION_NAMESPACE = re.compile(r"^[A-Za-z][A-Za-z0-9.-]{0,127}$")
_SECRET = re.compile(r"password|secret|credential|cookie|jwt|username|stream_uri", re.I)
class SourceConfigError(ValueError):
def __init__(self, code: str) -> None:
super().__init__(code)
self.code = code
@dataclass(frozen=True, slots=True)
class AppliedConfig:
config_id: str
revision: int
logical_device_id: str
media_ref: str
profile_encoding: str
frame_rate: float
rule_state: str
rules: RuleSet | None
@dataclass(frozen=True, slots=True)
class ApplyResult:
config_id: str
revision: int
state: str
config: AppliedConfig | None
class SourceConfigConsumer:
"""Persists validated snapshots before atomically changing the active pointer."""
def __init__(self, state_path: str | Path, *, clock: Callable[[], float] | None = None) -> None:
self._path = str(state_path)
self._clock = clock or time.time
self._lock = threading.RLock()
with closing(self._connect()) as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS source_snapshots (
config_id TEXT NOT NULL, revision INTEGER NOT NULL, effective_at INTEGER NOT NULL,
state TEXT NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(config_id, revision));
CREATE TABLE IF NOT EXISTS source_active (
config_id TEXT PRIMARY KEY, revision INTEGER NOT NULL,
FOREIGN KEY(config_id, revision) REFERENCES source_snapshots(config_id, revision));
"""
)
connection.commit()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self._path, timeout=5)
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA journal_mode=WAL")
return connection
def apply(self, body: bytes) -> ApplyResult:
document = _parse_and_validate(body)
config_id, revision = document["config_id"], document["revision"]
mapped = _map(document)
effective_at = int(_timestamp(document["effective_at"]))
with self._lock, closing(self._connect()) as connection:
connection.execute("BEGIN IMMEDIATE")
latest = connection.execute(
"SELECT revision, payload, state, effective_at FROM source_snapshots WHERE config_id=? ORDER BY revision DESC LIMIT 1",
(config_id,),
).fetchone()
if latest and revision < latest[0]:
connection.rollback()
raise SourceConfigError("STALE_REVISION")
canonical = body.decode("utf-8")
if latest and revision == latest[0]:
if json.loads(latest[1]) != document:
connection.rollback()
raise SourceConfigError("REVISION_CONFLICT")
connection.rollback()
return ApplyResult(config_id, revision, "idempotent", self.get_active(config_id))
connection.execute(
"INSERT INTO source_snapshots(config_id, revision, effective_at, state, payload) VALUES (?, ?, ?, ?, ?)",
(config_id, revision, effective_at, document["rule_set"]["state"], canonical),
)
if effective_at <= int(self._clock()):
connection.execute(
"INSERT INTO source_active(config_id, revision) VALUES (?, ?) ON CONFLICT(config_id) DO UPDATE SET revision=excluded.revision",
(config_id, revision),
)
state = "applied"
else:
state = "scheduled"
connection.commit()
return ApplyResult(config_id, revision, state, mapped if state == "applied" else self.get_active(config_id))
def activate_due(self) -> tuple[AppliedConfig, ...]:
now = int(self._clock())
activated: list[AppliedConfig] = []
with self._lock, closing(self._connect()) as connection:
connection.execute("BEGIN IMMEDIATE")
rows = connection.execute(
"SELECT s.payload FROM source_snapshots s JOIN (SELECT config_id, MAX(revision) revision FROM source_snapshots WHERE effective_at<=? GROUP BY config_id) d ON d.config_id=s.config_id AND d.revision=s.revision",
(now,),
).fetchall()
for (payload,) in rows:
document = json.loads(payload)
connection.execute(
"INSERT INTO source_active(config_id, revision) VALUES (?, ?) ON CONFLICT(config_id) DO UPDATE SET revision=excluded.revision",
(document["config_id"], document["revision"]),
)
activated.append(_map(document))
connection.commit()
return tuple(activated)
def get_active(self, config_id: str) -> AppliedConfig | None:
with closing(self._connect()) as connection:
row = connection.execute(
"SELECT s.payload FROM source_active a JOIN source_snapshots s ON s.config_id=a.config_id AND s.revision=a.revision WHERE a.config_id=?",
(config_id,),
).fetchone()
return _map(json.loads(row[0])) if row else None
def _parse_and_validate(body: bytes) -> dict[str, object]:
try:
document = json.loads(body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise SourceConfigError("CONFIG_INVALID") from None
if not isinstance(document, dict):
raise SourceConfigError("CONFIG_INVALID")
if document.get("schema_version") != VERSION:
raise SourceConfigError("UNSUPPORTED_SCHEMA_VERSION")
if _contains_secret(document):
raise SourceConfigError("CONFIG_INVALID")
required = {"schema_version", "config_id", "revision", "published_at", "effective_at", "site", "logical_device", "profile", "media", "rule_set", "integrity"}
if set(document) - (required | {"extensions"}) or not required <= set(document):
raise SourceConfigError("CONFIG_INVALID")
extensions = document.get("extensions", {})
if not isinstance(extensions, dict) or any(
not isinstance(namespace, str)
or not _EXTENSION_NAMESPACE.fullmatch(namespace)
or not isinstance(value, dict)
for namespace, value in extensions.items()
):
raise SourceConfigError("CONFIG_INVALID")
integrity = document.get("integrity")
if not isinstance(integrity, dict) or set(integrity) != {"algorithm", "value"} or integrity.get("algorithm") != "sha256":
raise SourceConfigError("CONFIG_INVALID")
unsigned = dict(document); unsigned.pop("integrity")
digest = hashlib.sha256(json.dumps(unsigned, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode()).hexdigest()
if not hmac.compare_digest(digest, str(integrity.get("value", ""))):
raise SourceConfigError("CONFIG_INVALID")
try:
if not _ID.fullmatch(document["config_id"]) or isinstance(document["revision"], bool) or document["revision"] < 1:
raise ValueError
published, effective = _timestamp(document["published_at"]), _timestamp(document["effective_at"])
if effective < published:
raise ValueError
for field in ("site", "logical_device"):
if not isinstance(document[field], dict) or set(document[field]) != {"id"} or not _ID.fullmatch(document[field]["id"]): raise ValueError
_validate_profile(document["profile"])
media = document["media"]
if not isinstance(media, dict) or set(media) != {"ref", "transport"} or media["transport"] != "rtsp" or not isinstance(media["ref"], str) or not media["ref"].startswith("media:") or any(marker in media["ref"] for marker in ("?", "#", "@", "\\", "://")): raise ValueError
_validate_rules(document["rule_set"], document["profile"])
except (KeyError, TypeError, ValueError, AttributeError):
raise SourceConfigError("CONFIG_INVALID") from None
return document
def _validate_profile(profile: object) -> None:
if not isinstance(profile, dict) or set(profile) != {"id", "width", "height", "encoding", "frame_rate"}: raise ValueError
if not _ID.fullmatch(profile["id"]) or profile["encoding"] not in {"H264", "H265", "MJPEG"}: raise ValueError
for field in ("width", "height"):
if isinstance(profile[field], bool) or not isinstance(profile[field], int) or profile[field] < 1: raise ValueError
if isinstance(profile["frame_rate"], bool) or not isinstance(profile["frame_rate"], (int, float)) or profile["frame_rate"] <= 0: raise ValueError
def _validate_rules(rules: object, profile: Mapping[str, object]) -> None:
if not isinstance(rules, dict) or set(rules) != {"version", "state", "profile_binding", "areas", "directional_lines"}: raise ValueError
if not _ID.fullmatch(rules["version"]) or rules["state"] not in {"active", "disabled", "recalibration_required"}: raise ValueError
binding = rules["profile_binding"]
if binding != {"profile_id": profile["id"], "width": profile["width"], "height": profile["height"]}: raise ValueError
if not isinstance(rules["areas"], list) or not isinstance(rules["directional_lines"], list) or len(rules["areas"]) > 1024 or len(rules["directional_lines"]) > 1024: raise ValueError
identifiers: set[str] = set()
for area in rules["areas"]:
if not isinstance(area, dict) or set(area) != {"id", "version", "kind", "enabled", "points"} or area["kind"] != "danger_area" or not isinstance(area["enabled"], bool) or not 3 <= len(area["points"]) <= 256: raise ValueError
_rule_identity(area, identifiers); points = tuple(_point(value) for value in area["points"])
polygon = sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(points, points[1:]+points[:1])) / 2
if abs(polygon) < 1e-9: raise ValueError
for line in rules["directional_lines"]:
if not isinstance(line, dict) or set(line) != {"id", "version", "kind", "enabled", "start", "end", "trigger_direction"} or line["kind"] != "directional_line" or not isinstance(line["enabled"], bool) or line["trigger_direction"] not in {"left_to_right", "right_to_left"}: raise ValueError
_rule_identity(line, identifiers)
if _point(line["start"]) == _point(line["end"]): raise ValueError
def _rule_identity(rule: Mapping[str, object], identifiers: set[str]) -> None:
if not isinstance(rule["id"], str) or not _ID.fullmatch(rule["id"]) or rule["id"] in identifiers or isinstance(rule["version"], bool) or not isinstance(rule["version"], int) or rule["version"] < 1: raise ValueError
identifiers.add(rule["id"])
def _point(value: object) -> tuple[float, float]:
if not isinstance(value, dict) or set(value) != {"x", "y"}: raise ValueError
x, y = value["x"], value["y"]
if isinstance(x, bool) or isinstance(y, bool) or not isinstance(x, (int,float)) or not isinstance(y,(int,float)) or not 0 <= x <= 1 or not 0 <= y <= 1: raise ValueError
return float(x), float(y)
def _map(document: Mapping[str, object]) -> AppliedConfig:
profile, rules = document["profile"], document["rule_set"]
rule_set = None
if rules["state"] == "active":
rule_set = RuleSet(
version=rules["version"], profile_id=profile["id"], width=profile["width"], height=profile["height"],
areas=tuple(AreaDefinition(a["id"], tuple(NormalizedPoint(**p) for p in a["points"])) for a in rules["areas"] if a["enabled"]),
directional_lines=tuple(DirectionalLineDefinition(l["id"], NormalizedPoint(**l["start"]), NormalizedPoint(**l["end"]), l["trigger_direction"]) for l in rules["directional_lines"] if l["enabled"]),
)
return AppliedConfig(document["config_id"], document["revision"], document["logical_device"]["id"], document["media"]["ref"], profile["encoding"], float(profile["frame_rate"]), rules["state"], rule_set)
def _timestamp(value: object) -> float:
if not isinstance(value, str) or not value.endswith("Z"): raise ValueError
return datetime.fromisoformat(value[:-1] + "+00:00").astimezone(timezone.utc).timestamp()
def _contains_secret(value: object) -> bool:
if isinstance(value, dict): return any(_SECRET.search(str(k)) or _contains_secret(v) for k,v in value.items())
if isinstance(value, list): return any(_contains_secret(item) for item in value)
if isinstance(value, str):
split=urlsplit(value)
return bool(split.username or split.password or value.startswith("file:") or re.match(r"^[A-Za-z]:[\\/]", value))
return False
@@ -0,0 +1,44 @@
"""Brain-owned durable replay storage; never shared with Sense or Bell."""
from __future__ import annotations
import sqlite3
import threading
from contextlib import closing
from pathlib import Path
class SQLiteReplayStore:
def __init__(self, path: str | Path) -> None:
self._path = str(path)
self._lock = threading.Lock()
with closing(self._connect()) as connection:
connection.execute("PRAGMA journal_mode=WAL")
connection.execute(
"CREATE TABLE IF NOT EXISTS machine_replay (principal TEXT NOT NULL, token_id TEXT NOT NULL, expires_at INTEGER NOT NULL, PRIMARY KEY(principal, token_id))"
)
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self._path, timeout=5, isolation_level=None)
connection.execute("PRAGMA busy_timeout=5000")
return connection
def consume(self, principal: str, token_id: str, expires_at: int, now: int) -> bool:
if not principal or not token_id or expires_at <= now:
return False
with self._lock, closing(self._connect()) as connection:
try:
connection.execute("BEGIN IMMEDIATE")
connection.execute("DELETE FROM machine_replay WHERE expires_at <= ?", (now,))
connection.execute(
"INSERT INTO machine_replay(principal, token_id, expires_at) VALUES (?, ?, ?)",
(principal, token_id, expires_at),
)
connection.execute("COMMIT")
return True
except sqlite3.IntegrityError:
connection.execute("ROLLBACK")
return False
except Exception:
connection.execute("ROLLBACK")
return False
@@ -0,0 +1,77 @@
"""Persistent sequence allocation and safe runtime-status/v1 production."""
from __future__ import annotations
import json
import re
import sqlite3
import threading
import uuid
from contextlib import closing
from datetime import datetime, timezone
from pathlib import Path
from typing import Mapping, Sequence
_LOGICAL_REF = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
_VERSION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$")
_ERROR_CODE = re.compile(r"^[A-Z][A-Z0-9_]{2,63}$")
_RUNTIME_STATES = {"unconfigured", "starting", "running", "degraded", "failed", "stopped"}
class RuntimeStatusPublisher:
def __init__(self, state_path: str | Path, brain_instance_ref: str) -> None:
self._path, self._instance, self._lock = str(state_path), brain_instance_ref, threading.Lock()
with closing(self._connect()) as connection:
connection.execute("CREATE TABLE IF NOT EXISTS runtime_sequence(instance_ref TEXT PRIMARY KEY, sequence INTEGER NOT NULL)")
connection.commit()
def _connect(self) -> sqlite3.Connection:
return sqlite3.connect(self._path, timeout=5)
def build(self, *, runtime_state: str, runtime_version: str, started_at: datetime | None, model_ref: str, model_version: str, configurations: Sequence[Mapping[str, object]], health: Mapping[str, object], inputs: Sequence[Mapping[str, object]], observed_at: datetime | None = None) -> bytes:
_validate(runtime_state, runtime_version, self._instance, model_ref, model_version, configurations, health, inputs)
with self._lock, closing(self._connect()) as connection:
connection.execute("BEGIN IMMEDIATE")
row=connection.execute("SELECT sequence FROM runtime_sequence WHERE instance_ref=?",(self._instance,)).fetchone();sequence=(row[0]+1) if row else 0
connection.execute("INSERT INTO runtime_sequence(instance_ref,sequence) VALUES(?,?) ON CONFLICT(instance_ref) DO UPDATE SET sequence=excluded.sequence",(self._instance,sequence));connection.commit()
observed=(observed_at or datetime.now(timezone.utc)).astimezone(timezone.utc)
document={"schema_version":"yovision.runtime-status/v1","status_id":str(uuid.uuid4()),"brain_instance_ref":self._instance,"sequence":sequence,"observed_at":_utc(observed),"runtime":{"state":runtime_state,"version":runtime_version,"started_at":_utc(started_at) if started_at else None},"model":{"model_ref":model_ref,"version":model_version},"configurations":list(configurations),"health":dict(health),"inputs":list(inputs)}
raw=json.dumps(document,separators=(",",":"),sort_keys=True).encode()
lowered=raw.lower();
for marker in (b"password",b"credential",b"stream_uri",b"cookie",b"jwt",b"file://"):
if marker in lowered: raise ValueError("runtime status contains sensitive field")
return raw
def _utc(value: datetime) -> str:
if value.tzinfo is None: raise ValueError("runtime timestamp must be timezone-aware")
return value.astimezone(timezone.utc).isoformat(timespec="seconds").replace("+00:00","Z")
def _validate(runtime_state: str, runtime_version: str, instance: str, model_ref: str, model_version: str, configurations: Sequence[Mapping[str, object]], health: Mapping[str, object], inputs: Sequence[Mapping[str, object]]) -> None:
if runtime_state not in _RUNTIME_STATES or not _VERSION.fullmatch(runtime_version) or not _LOGICAL_REF.fullmatch(instance) or not _LOGICAL_REF.fullmatch(model_ref) or not _VERSION.fullmatch(model_version):
raise ValueError("invalid runtime identity or version")
if len(configurations) > 4096 or len(inputs) > 4096:
raise ValueError("runtime status collection too large")
seen: set[str] = set()
for item in configurations:
if set(item) != {"config_id", "apply_state", "applied_revision", "error_code"} or not isinstance(item["config_id"], str) or not _LOGICAL_REF.fullmatch(item["config_id"]) or item["config_id"] in seen:
raise ValueError("invalid configuration status")
seen.add(item["config_id"]); state=item["apply_state"]; revision=item["applied_revision"]; error=item["error_code"]
if state not in {"not_configured","applying","applied","rejected"} or (state=="not_configured" and revision is not None) or (state=="applied" and (isinstance(revision,bool) or not isinstance(revision,int) or revision<1)) or (state=="rejected" and (not isinstance(error,str) or not _ERROR_CODE.fullmatch(error))):
raise ValueError("invalid configuration status")
if set(health) != {"overall","error_codes","metrics"} or health["overall"] not in {"healthy","degraded","unhealthy"} or not _codes(health["error_codes"],32) or not _metrics(health["metrics"]):
raise ValueError("invalid health status")
for item in inputs:
if set(item) != {"input_ref","state","error_codes","metrics"} or not isinstance(item["input_ref"],str) or not _LOGICAL_REF.fullmatch(item["input_ref"]) or item["state"] not in _RUNTIME_STATES or not _codes(item["error_codes"],16) or not _metrics(item["metrics"]):
raise ValueError("invalid input status")
def _codes(value: object, limit: int) -> bool:
return isinstance(value,list) and len(value)<=limit and len(set(value))==len(value) and all(isinstance(code,str) and _ERROR_CODE.fullmatch(code) for code in value)
def _metrics(value: object) -> bool:
if not isinstance(value,Mapping) or set(value)!={"load_percent","queue_depth","latency_ms"}: return False
load,queue,latency=value["load_percent"],value["queue_depth"],value["latency_ms"]
return not isinstance(load,bool) and isinstance(load,(int,float)) and 0<=load<=100 and not isinstance(queue,bool) and isinstance(queue,int) and queue>=0 and not isinstance(latency,bool) and isinstance(latency,(int,float)) and latency>=0
@@ -0,0 +1,101 @@
from __future__ import annotations
import hashlib
import json
import threading
from datetime import datetime, timedelta, timezone
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from yovision_brain.integration.machine_identity.token import KeyRecord, Registry, Signer, Verifier
from yovision_brain.integration.sense_control.connector import ConnectorResponse, SourceConfigEndpoint, StatusSender
from yovision_brain.integration.sense_control.consumer import SourceConfigConsumer, SourceConfigError
from yovision_brain.integration.sense_control.replay import SQLiteReplayStore
from yovision_brain.integration.sense_control.status import RuntimeStatusPublisher
def source_document(revision: int = 1, *, state: str = "active", effective: int = 0) -> bytes:
now=datetime(2026,8,31,tzinfo=timezone.utc)
document={"schema_version":"yovision.source-config/v1","config_id":"gate-primary","revision":revision,"published_at":now.isoformat().replace("+00:00","Z"),"effective_at":(now+timedelta(seconds=effective)).isoformat().replace("+00:00","Z"),"site":{"id":"site-east"},"logical_device":{"id":"camera-1"},"profile":{"id":"main","width":1920,"height":1080,"encoding":"H264","frame_rate":25},"media":{"ref":"media:site-east/camera-1/main","transport":"rtsp"},"rule_set":{"version":f"rules-{revision}","state":state,"profile_binding":{"profile_id":"main","width":1920,"height":1080},"areas":[{"id":"danger","version":1,"kind":"danger_area","enabled":True,"points":[{"x":.1,"y":.1},{"x":.8,"y":.1},{"x":.5,"y":.8}]}],"directional_lines":[]}}
digest=hashlib.sha256(json.dumps(document,separators=(",",":"),sort_keys=True).encode()).hexdigest();document["integrity"]={"algorithm":"sha256","value":digest}
return json.dumps(document,separators=(",",":"),sort_keys=True).encode()
def with_extension(body: bytes, extensions: object) -> bytes:
document=json.loads(body);document["extensions"]=extensions;unsigned=dict(document);unsigned.pop("integrity");document["integrity"]["value"]=hashlib.sha256(json.dumps(unsigned,separators=(",",":"),sort_keys=True).encode()).hexdigest();return json.dumps(document,separators=(",",":"),sort_keys=True).encode()
def identity(tmp_path, now: int):
private=Ed25519PrivateKey.generate();signer=Signer("yv:sense:east","sense-key-01",private,clock=lambda:now)
registry=Registry([KeyRecord("yv:sense:east","sense-key-01",private.public_key(),"yovision-brain",frozenset({"source-config:write"}))])
replay=SQLiteReplayStore(tmp_path/"replay.sqlite")
return signer,Verifier(registry,replay,clock=lambda:now)
def test_authenticated_apply_is_idempotent_and_replay_survives_restart(tmp_path):
now=int(datetime(2026,8,31,tzinfo=timezone.utc).timestamp());signer,verifier=identity(tmp_path,now);body=source_document();consumer=SourceConfigConsumer(tmp_path/"state.sqlite",clock=lambda:now);endpoint=SourceConfigEndpoint(consumer,verifier)
token=signer.mint("yovision-brain",("source-config:write",),"POST","/machine/v1/source-config",body)
result=endpoint.receive("Bearer "+token,body,"corr-request-0001");assert result.state=="applied" and result.config.rules is not None
with pytest.raises(ValueError,match="machine_token_replayed"):endpoint.receive("Bearer "+token,body,"corr-request-0001")
restarted=SourceConfigEndpoint(SourceConfigConsumer(tmp_path/"state.sqlite",clock=lambda:now),Verifier(verifier._registry,SQLiteReplayStore(tmp_path/"replay.sqlite"),clock=lambda:now))
with pytest.raises(ValueError,match="machine_token_replayed"):restarted.receive("Bearer "+token,body,"corr-request-0002")
new_token=signer.mint("yovision-brain",("source-config:write",),"POST","/machine/v1/source-config",body);assert restarted.receive("Bearer "+new_token,body,"corr-request-0003").state=="idempotent"
def test_atomic_replay_accepts_once_under_concurrency(tmp_path):
store=SQLiteReplayStore(tmp_path/"atomic.sqlite");results=[]
threads=[threading.Thread(target=lambda:results.append(store.consume("yv:sense:east","token-id",200,100))) for _ in range(12)]
for thread in threads:thread.start()
for thread in threads:thread.join()
assert results.count(True)==1
def test_stale_unknown_profile_and_recalibration_are_safe(tmp_path):
now=int(datetime(2026,8,31,tzinfo=timezone.utc).timestamp());consumer=SourceConfigConsumer(tmp_path/"state.sqlite",clock=lambda:now)
assert consumer.apply(source_document(2)).config.rules is not None
with pytest.raises(SourceConfigError,match="STALE_REVISION"):consumer.apply(source_document(1))
invalid=json.loads(source_document(3));invalid["profile"]["width"]=1280;invalid["integrity"]["value"]="0"*64
with pytest.raises(SourceConfigError,match="CONFIG_INVALID"):consumer.apply(json.dumps(invalid).encode())
unknown=json.loads(source_document(3));unknown["schema_version"]="yovision.source-config/v2"
with pytest.raises(SourceConfigError,match="UNSUPPORTED_SCHEMA_VERSION"):consumer.apply(json.dumps(unknown).encode())
safe=consumer.apply(source_document(3,state="recalibration_required"));assert safe.config.rule_state=="recalibration_required" and safe.config.rules is None
def test_future_effective_snapshot_activates_atomically_after_restart(tmp_path):
base=int(datetime(2026,8,31,tzinfo=timezone.utc).timestamp());current=[base];path=tmp_path/"state.sqlite";consumer=SourceConfigConsumer(path,clock=lambda:current[0])
assert consumer.apply(source_document(1)).state=="applied";scheduled=consumer.apply(source_document(2,effective=60));assert scheduled.state=="scheduled" and scheduled.config.revision==1
current[0]+=61;restarted=SourceConfigConsumer(path,clock=lambda:current[0]);activated=restarted.activate_due();assert activated[0].revision==2 and restarted.get_active("gate-primary").revision==2
def test_unknown_valid_extension_namespace_is_ignored(tmp_path):
now=int(datetime(2026,8,31,tzinfo=timezone.utc).timestamp());consumer=SourceConfigConsumer(tmp_path/"state.sqlite",clock=lambda:now)
result=consumer.apply(with_extension(source_document(),{"vendor.example":{"feature":"safe"}}));assert result.state=="applied" and result.config.revision==1
@pytest.mark.parametrize("extensions", [[], {"1invalid":{}}, {"vendor_ok":{}}, {"vendor.example":"not-an-object"}])
def test_invalid_extensions_are_rejected(tmp_path, extensions):
now=int(datetime(2026,8,31,tzinfo=timezone.utc).timestamp());consumer=SourceConfigConsumer(tmp_path/"state.sqlite",clock=lambda:now)
with pytest.raises(SourceConfigError,match="CONFIG_INVALID"):consumer.apply(with_extension(source_document(),extensions))
def test_status_sequence_restart_retry_timeout_and_disable(tmp_path):
path=tmp_path/"status.sqlite";publisher=RuntimeStatusPublisher(path,"brain-east-01");health={"overall":"healthy","error_codes":[],"metrics":{"load_percent":1.0,"queue_depth":0,"latency_ms":2.0}}
one=json.loads(publisher.build(runtime_state="running",runtime_version="1.0.0",started_at=datetime.now(timezone.utc),model_ref="people-detection",model_version="1",configurations=[],health=health,inputs=[]));two=json.loads(RuntimeStatusPublisher(path,"brain-east-01").build(runtime_state="running",runtime_version="1.0.0",started_at=None,model_ref="people-detection",model_version="1",configurations=[],health=health,inputs=[]));assert (one["sequence"],two["sequence"])==(0,1)
private=Ed25519PrivateKey.generate();signer=Signer("yv:brain:east","brain-key-01",private,clock=lambda:1_787_000_000);attempts=[]
def send(_auth,_body,_corr,timeout):attempts.append(timeout);raise TimeoutError
sender=StatusSender(signer,send,max_attempts=3,sleeper=lambda _:None)
with pytest.raises(RuntimeError,match="STATUS_DELIVERY_EXHAUSTED"):sender.publish(b"{}","corr-request-0001")
assert attempts==[5.0,5.0,5.0]
disabled=StatusSender(signer,lambda *_:ConnectorResponse(204,b"","corr-request-0001"),enabled=False)
with pytest.raises(RuntimeError,match="CONNECTOR_DISABLED"):disabled.publish(b"{}","corr-request-0001")
@pytest.mark.parametrize("request_id", ["short", "0123456789abcde\n", "0123456789abcde!", "a"*129])
def test_connector_rejects_unsafe_request_ids(tmp_path, request_id):
now=int(datetime(2026,8,31,tzinfo=timezone.utc).timestamp());signer,verifier=identity(tmp_path,now);body=source_document();endpoint=SourceConfigEndpoint(SourceConfigConsumer(tmp_path/"state.sqlite",clock=lambda:now),verifier)
token=signer.mint("yovision-brain",("source-config:write",),"POST","/machine/v1/source-config",body)
with pytest.raises(ValueError,match="INVALID_CORRELATION_ID"):endpoint.receive("Bearer "+token,body,request_id)
status_signer=Signer("yv:brain:east","brain-key-01",Ed25519PrivateKey.generate(),clock=lambda:now)
sender=StatusSender(status_signer,lambda *_:ConnectorResponse(204,b"",request_id))
with pytest.raises(ValueError,match="INVALID_CORRELATION_ID"):sender.publish(b"{}",request_id)
@@ -0,0 +1,118 @@
package brain_control
import (
"errors"
"fmt"
"regexp"
"time"
mi "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/machine_identity"
)
const (
sourceConfigPath = "/machine/v1/source-config"
runtimeStatusPath = "/machine/v1/runtime-status"
)
var requestIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{15,127}$`)
type SendResponse struct {
StatusCode int
Body []byte
CorrelationID string
}
type SendFunc func(authorization string, body []byte, correlationID string, timeout time.Duration) (SendResponse, error)
type ConfigSender struct {
Signer mi.Signer
Send SendFunc
Enabled bool
Timeout time.Duration
MaxAttempts int
Sleep func(time.Duration)
}
func (s ConfigSender) Publish(config SourceConfig, correlationID string) (SendResponse, error) {
if !s.Enabled {
return SendResponse{}, errors.New("CONNECTOR_DISABLED")
}
if s.Send == nil || !requestIDPattern.MatchString(correlationID) {
return SendResponse{}, errors.New("invalid connector configuration")
}
if s.Timeout <= 0 {
s.Timeout = 5 * time.Second
}
if s.MaxAttempts == 0 {
s.MaxAttempts = 4
}
if s.MaxAttempts < 1 {
return SendResponse{}, errors.New("invalid connector retry policy")
}
if s.Sleep == nil {
s.Sleep = time.Sleep
}
if err := ValidateSourceConfig(config); err != nil {
return SendResponse{}, err
}
body, err := MarshalSourceConfig(config)
if err != nil {
return SendResponse{}, err
}
var last error
for attempt := 0; attempt < s.MaxAttempts; attempt++ {
token, mintErr := s.Signer.Mint("yovision-brain", []string{"source-config:write"}, "POST", sourceConfigPath, body)
if mintErr != nil {
return SendResponse{}, mintErr
}
response, sendErr := s.Send("Bearer "+token, body, correlationID, s.Timeout)
if sendErr == nil && response.StatusCode >= 200 && response.StatusCode < 300 {
return response, nil
}
if sendErr == nil && response.StatusCode < 500 {
return SendResponse{}, fmt.Errorf("source config rejected: %d", response.StatusCode)
}
if sendErr != nil {
last = sendErr
} else {
last = fmt.Errorf("source config remote status: %d", response.StatusCode)
}
if attempt+1 < s.MaxAttempts {
delay := time.Second << attempt
if delay > 30*time.Second {
delay = 30 * time.Second
}
s.Sleep(delay)
}
}
return SendResponse{}, fmt.Errorf("source config delivery exhausted: %w", last)
}
type RuntimeStatusEndpoint struct {
Verifier mi.Verifier
Store ProjectionStore
Enabled bool
MaxBodyBytes int
}
func (e RuntimeStatusEndpoint) Receive(authorization string, body []byte, correlationID string, expected map[string]int64) (ProjectionView, error) {
if !e.Enabled {
return ProjectionView{}, errors.New("CONNECTOR_DISABLED")
}
if !requestIDPattern.MatchString(correlationID) {
return ProjectionView{}, errors.New("INVALID_CORRELATION_ID")
}
if e.MaxBodyBytes == 0 {
e.MaxBodyBytes = 10 * 1024 * 1024
}
if len(body) > e.MaxBodyBytes {
return ProjectionView{}, errors.New("REQUEST_TOO_LARGE")
}
token, err := mi.BearerToken(authorization)
if err != nil {
return ProjectionView{}, err
}
if _, err = e.Verifier.Verify(token, "yovision-sense", "runtime-status:write", "POST", runtimeStatusPath, body); err != nil {
return ProjectionView{}, err
}
return e.Store.Ingest(body, expected)
}
@@ -0,0 +1,124 @@
package brain_control
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"strings"
"sync"
"testing"
"time"
mi "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/machine_identity"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func memoryDB(t *testing.T, name string) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+name+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ReplayToken{}, &RuntimeProjection{}, &SourceRevision{}); err != nil {
t.Fatal(err)
}
return db
}
func TestSourceMapperAndRecalibration(t *testing.T) {
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
facts := SourceFacts{ConfigID: "gate-primary", SiteID: "site-east", LogicalDeviceID: "camera-1", MediaPath: "site/camera/main", Revision: 1, PublishedAt: now, EffectiveAt: now, Profile: Profile{ID: "main", Width: 1920, Height: 1080, Encoding: "h264", FrameRate: 25}, RuleSetVersion: "rules-1", Areas: []AreaRule{{ID: "danger", Version: 1, Kind: "danger_area", Enabled: true, Points: []Point{{.1, .1}, {.8, .1}, {.5, .8}}}}}
config, err := MapSourceConfig(facts)
if err != nil {
t.Fatal(err)
}
if err = ValidateSourceConfig(config); err != nil {
t.Fatal(err)
}
facts.Revision = 2
facts.NeedsRecalibration = true
config, err = MapSourceConfig(facts)
if err != nil {
t.Fatal(err)
}
if config.RuleSet.State != "recalibration_required" || config.RuleSet.Areas[0].Enabled {
t.Fatal("recalibration did not disable rules")
}
}
func TestReplayAtomicAndRestart(t *testing.T) {
db := memoryDB(t, "replay-package")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
registry, _ := mi.NewRegistry(mi.KeyRecord{Principal: "yv:brain:east", KeyID: "brain-key-01", PublicKey: pub, Audience: "yovision-sense", Scopes: []string{"runtime-status:write"}, Enabled: true})
signer := mi.Signer{Principal: "yv:brain:east", KeyID: "brain-key-01", PrivateKey: priv, Now: func() time.Time { return now }}
body := []byte("{}")
token, _ := signer.Mint("yovision-sense", []string{"runtime-status:write"}, "POST", "/machine/v1/runtime-status", body)
accepted := 0
var mu sync.Mutex
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
v := mi.Verifier{Registry: registry, Replay: GORMReplayStore{DB: db}, Now: func() time.Time { return now }}
if _, err := v.Verify(token, "yovision-sense", "runtime-status:write", "POST", "/machine/v1/runtime-status", body); err == nil {
mu.Lock()
accepted++
mu.Unlock()
}
}()
}
wg.Wait()
if accepted != 1 {
t.Fatalf("accepted %d", accepted)
}
v := mi.Verifier{Registry: registry, Replay: GORMReplayStore{DB: db}, Now: func() time.Time { return now }}
if _, err := v.Verify(token, "yovision-sense", "runtime-status:write", "POST", "/machine/v1/runtime-status", body); err == nil {
t.Fatal("restart replay accepted")
}
}
func TestProjectionStaleRecoveryAndMismatch(t *testing.T) {
db := memoryDB(t, "projection-package")
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
store := ProjectionStore{DB: db, Clock: func() time.Time { return now }, StaleAfter: 90 * time.Second}
raw := runtimeFixture("018f4d6a-8d1b-4a25-8b37-9085f9c0d101", 1, now, 2)
view, err := store.Ingest(raw, map[string]int64{"gate": 3})
if err != nil || !view.RevisionMismatch {
t.Fatalf("view %+v err %v", view, err)
}
now = now.Add(91 * time.Second)
view, _ = store.View("brain-east-01")
if !view.Offline {
t.Fatal("not offline")
}
raw = runtimeFixture("018f4d6a-8d1b-4a25-8b37-9085f9c0d102", 2, now, 3)
view, err = store.Ingest(raw, map[string]int64{"gate": 3})
if err != nil || !view.Recovered {
t.Fatalf("recovery %+v err %v", view, err)
}
}
func TestConnectorRejectsUnsafeRequestIDs(t *testing.T) {
for _, value := range []string{"short", "0123456789abcde\n", "0123456789abcde!", strings.Repeat("a", 129)} {
sender := ConfigSender{Enabled: true, Send: func(string, []byte, string, time.Duration) (SendResponse, error) {
t.Fatal("unsafe request id reached transport")
return SendResponse{}, nil
}}
if _, err := sender.Publish(SourceConfig{}, value); err == nil {
t.Fatalf("sender accepted request id %q", value)
}
endpoint := RuntimeStatusEndpoint{Enabled: true}
if _, err := endpoint.Receive("Bearer ignored", nil, value, nil); err == nil || err.Error() != "INVALID_CORRELATION_ID" {
t.Fatalf("endpoint accepted request id %q: %v", value, err)
}
}
}
func runtimeFixture(id string, seq int64, observed time.Time, revision int64) []byte {
value := map[string]any{"schema_version": RuntimeStatusVersion, "status_id": id, "brain_instance_ref": "brain-east-01", "sequence": seq, "observed_at": observed.Format(time.RFC3339), "runtime": map[string]any{"state": "running", "version": "1.0.0", "started_at": observed.Format(time.RFC3339)}, "model": map[string]any{"model_ref": "people", "version": "1"}, "configurations": []any{map[string]any{"config_id": "gate", "apply_state": "applied", "applied_revision": revision, "error_code": nil}}, "health": map[string]any{"overall": "healthy", "error_codes": []any{}, "metrics": map[string]any{"load_percent": 1.0, "queue_depth": 0, "latency_ms": 1.0}}, "inputs": []any{}}
raw, _ := json.Marshal(value)
return raw
}
@@ -0,0 +1,135 @@
package brain_control
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
)
var stableID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$`)
func MapSourceConfig(f SourceFacts) (SourceConfig, error) {
if !stableID.MatchString(f.ConfigID) || !stableID.MatchString(f.SiteID) || !stableID.MatchString(f.LogicalDeviceID) || !stableID.MatchString(f.Profile.ID) || f.Revision < 1 || f.Profile.Width < 1 || f.Profile.Height < 1 || f.Profile.FrameRate <= 0 {
return SourceConfig{}, errors.New("invalid source configuration facts")
}
encoding := strings.ToUpper(f.Profile.Encoding)
if encoding != "H264" && encoding != "H265" && encoding != "MJPEG" {
return SourceConfig{}, errors.New("unsupported profile encoding")
}
if f.PublishedAt.IsZero() || f.EffectiveAt.Before(f.PublishedAt) {
return SourceConfig{}, errors.New("invalid source configuration time")
}
if !stableID.MatchString(f.RuleSetVersion) {
return SourceConfig{}, errors.New("invalid rule set version")
}
if strings.ContainsAny(f.MediaPath, "?#@\\") || strings.Contains(f.MediaPath, "://") || f.MediaPath == "" {
return SourceConfig{}, errors.New("media path must be opaque and credential-free")
}
if len(f.Areas) > 1024 || len(f.DirectionalLines) > 1024 {
return SourceConfig{}, errors.New("too many rules")
}
seen := map[string]bool{}
for _, a := range f.Areas {
if !stableID.MatchString(a.ID) || seen[a.ID] || a.Version < 1 || a.Kind != "danger_area" || len(a.Points) < 3 || len(a.Points) > 256 || !validPoints(a.Points) || polygonArea(a.Points) == 0 {
return SourceConfig{}, errors.New("invalid area rule")
}
seen[a.ID] = true
}
for _, l := range f.DirectionalLines {
if !stableID.MatchString(l.ID) || seen[l.ID] || l.Version < 1 || l.Kind != "directional_line" || (l.TriggerDirection != "left_to_right" && l.TriggerDirection != "right_to_left") || !validPoints([]Point{l.Start, l.End}) || l.Start == l.End {
return SourceConfig{}, errors.New("invalid directional line rule")
}
seen[l.ID] = true
}
var out SourceConfig
out.SchemaVersion, out.ConfigID, out.Revision = SourceConfigVersion, f.ConfigID, f.Revision
out.PublishedAt, out.EffectiveAt = f.PublishedAt.UTC(), f.EffectiveAt.UTC()
out.Site.ID, out.LogicalDevice.ID = f.SiteID, f.LogicalDeviceID
out.Profile = f.Profile
out.Profile.Encoding = encoding
out.Media.Ref, out.Media.Transport = "media:"+strings.TrimPrefix(f.MediaPath, "/"), "rtsp"
out.RuleSet.Version = f.RuleSetVersion
out.RuleSet.State = "active"
if f.Disabled {
out.RuleSet.State = "disabled"
}
if f.NeedsRecalibration {
out.RuleSet.State = "recalibration_required"
}
out.RuleSet.ProfileBinding.ProfileID, out.RuleSet.ProfileBinding.Width, out.RuleSet.ProfileBinding.Height = f.Profile.ID, f.Profile.Width, f.Profile.Height
out.RuleSet.Areas = append([]AreaRule(nil), f.Areas...)
out.RuleSet.DirectionalLines = append([]DirectionalLineRule(nil), f.DirectionalLines...)
if out.RuleSet.State != "active" {
for i := range out.RuleSet.Areas {
out.RuleSet.Areas[i].Enabled = false
}
for i := range out.RuleSet.DirectionalLines {
out.RuleSet.DirectionalLines[i].Enabled = false
}
}
digest, err := sourceDigest(out)
if err != nil {
return SourceConfig{}, err
}
out.Integrity.Algorithm, out.Integrity.Value = "sha256", digest
return out, nil
}
func MarshalSourceConfig(config SourceConfig) ([]byte, error) { return json.Marshal(config) }
func sourceDigest(config SourceConfig) (string, error) {
raw, err := json.Marshal(config)
if err != nil {
return "", err
}
var value map[string]any
if err = json.Unmarshal(raw, &value); err != nil {
return "", err
}
delete(value, "integrity")
canonical, err := json.Marshal(value)
if err != nil {
return "", err
}
sum := sha256.Sum256(canonical)
return hex.EncodeToString(sum[:]), nil
}
func validPoints(points []Point) bool {
for _, p := range points {
if p.X < 0 || p.X > 1 || p.Y < 0 || p.Y > 1 {
return false
}
}
return true
}
func polygonArea(p []Point) float64 {
var a float64
for i := range p {
n := p[(i+1)%len(p)]
a += p[i].X*n.Y - n.X*p[i].Y
}
if a < 0 {
a = -a
}
return a / 2
}
func ValidateSourceConfig(config SourceConfig) error {
if config.SchemaVersion != SourceConfigVersion {
return fmt.Errorf("unsupported source config version")
}
digest, err := sourceDigest(config)
if err != nil || config.Integrity.Algorithm != "sha256" || digest != config.Integrity.Value {
return errors.New("source config integrity mismatch")
}
_, err = MapSourceConfig(SourceFacts{ConfigID: config.ConfigID, SiteID: config.Site.ID, LogicalDeviceID: config.LogicalDevice.ID, MediaPath: strings.TrimPrefix(config.Media.Ref, "media:"), Revision: config.Revision, PublishedAt: config.PublishedAt, EffectiveAt: config.EffectiveAt, Profile: config.Profile, RuleSetVersion: config.RuleSet.Version, Disabled: config.RuleSet.State == "disabled", NeedsRecalibration: config.RuleSet.State == "recalibration_required", Areas: config.RuleSet.Areas, DirectionalLines: config.RuleSet.DirectionalLines})
return err
}
func UTCNow() time.Time { return time.Now().UTC() }
@@ -0,0 +1,122 @@
package brain_control
import "time"
const (
SourceConfigVersion = "yovision.source-config/v1"
RuntimeStatusVersion = "yovision.runtime-status/v1"
)
type Point struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
type Profile struct {
ID string `json:"id"`
Width int `json:"width"`
Height int `json:"height"`
Encoding string `json:"encoding"`
FrameRate float64 `json:"frame_rate"`
}
type AreaRule struct {
ID string `json:"id"`
Version int64 `json:"version"`
Kind string `json:"kind"`
Enabled bool `json:"enabled"`
Points []Point `json:"points"`
}
type DirectionalLineRule struct {
ID string `json:"id"`
Version int64 `json:"version"`
Kind string `json:"kind"`
Enabled bool `json:"enabled"`
Start Point `json:"start"`
End Point `json:"end"`
TriggerDirection string `json:"trigger_direction"`
}
type SourceConfig struct {
SchemaVersion string `json:"schema_version"`
ConfigID string `json:"config_id"`
Revision int64 `json:"revision"`
PublishedAt time.Time `json:"published_at"`
EffectiveAt time.Time `json:"effective_at"`
Site struct {
ID string `json:"id"`
} `json:"site"`
LogicalDevice struct {
ID string `json:"id"`
} `json:"logical_device"`
Profile Profile `json:"profile"`
Media struct {
Ref string `json:"ref"`
Transport string `json:"transport"`
} `json:"media"`
RuleSet struct {
Version string `json:"version"`
State string `json:"state"`
ProfileBinding struct {
ProfileID string `json:"profile_id"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"profile_binding"`
Areas []AreaRule `json:"areas"`
DirectionalLines []DirectionalLineRule `json:"directional_lines"`
} `json:"rule_set"`
Integrity struct {
Algorithm string `json:"algorithm"`
Value string `json:"value"`
} `json:"integrity"`
}
// SourceFacts is an explicit, credential-free boundary DTO. Callers map their
// GORM entities into it; database models are never serialized as a contract.
type SourceFacts struct {
ConfigID, SiteID, LogicalDeviceID, MediaRouteID, MediaPath string
Revision int64
PublishedAt, EffectiveAt time.Time
Profile Profile
RuleSetVersion string
Disabled, NeedsRecalibration bool
Areas []AreaRule
DirectionalLines []DirectionalLineRule
}
type ReplayToken struct {
Principal string `gorm:"size:128;primaryKey"`
TokenID string `gorm:"size:96;primaryKey"`
ExpiresAt time.Time `gorm:"not null;index"`
CreatedAt time.Time `gorm:"not null"`
}
func (ReplayToken) TableName() string { return "sense_brain_runtime_replay_tokens" }
type RuntimeProjection struct {
BrainInstanceRef string `gorm:"size:128;primaryKey"`
StatusID string `gorm:"size:36;not null;uniqueIndex"`
Sequence int64 `gorm:"not null"`
ObservedAt time.Time `gorm:"not null;index"`
ReceivedAt time.Time `gorm:"not null"`
RuntimeState string `gorm:"size:32;not null"`
RuntimeVersion string `gorm:"size:64;not null"`
ModelRef string `gorm:"size:128;not null"`
ModelVersion string `gorm:"size:64;not null"`
HealthOverall string `gorm:"size:32;not null"`
ExpectedRevisionsJSON string `gorm:"type:jsonb;not null"`
ConfigurationsJSON string `gorm:"type:jsonb;not null"`
HealthJSON string `gorm:"type:jsonb;not null"`
InputsJSON string `gorm:"type:jsonb;not null"`
WasOffline bool `gorm:"not null;default:false"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (RuntimeProjection) TableName() string { return "sense_brain_runtime_projections" }
type SourceRevision struct {
ConfigID string `gorm:"size:128;primaryKey"`
Revision int64 `gorm:"not null"`
UpdatedAt time.Time
}
func (SourceRevision) TableName() string { return "sense_brain_source_revisions" }
@@ -0,0 +1,145 @@
package brain_control
import (
"bytes"
"encoding/json"
"errors"
"io"
"regexp"
"time"
)
type runtimeStatus struct {
SchemaVersion string `json:"schema_version"`
StatusID string `json:"status_id"`
BrainInstanceRef string `json:"brain_instance_ref"`
Sequence int64 `json:"sequence"`
ObservedAt time.Time `json:"observed_at"`
Runtime struct {
State string `json:"state"`
Version string `json:"version"`
StartedAt *time.Time `json:"started_at"`
} `json:"runtime"`
Model struct {
ModelRef string `json:"model_ref"`
Version string `json:"version"`
} `json:"model"`
Configurations []configurationStatus `json:"configurations"`
Health healthStatus `json:"health"`
Inputs []inputStatus `json:"inputs"`
}
type configurationStatus struct {
ConfigID string `json:"config_id"`
ApplyState string `json:"apply_state"`
AppliedRevision *int64 `json:"applied_revision"`
ErrorCode *string `json:"error_code"`
}
type metrics struct {
LoadPercent float64 `json:"load_percent"`
QueueDepth int64 `json:"queue_depth"`
LatencyMS float64 `json:"latency_ms"`
}
type healthStatus struct {
Overall string `json:"overall"`
ErrorCodes []string `json:"error_codes"`
Metrics metrics `json:"metrics"`
}
type inputStatus struct {
InputRef string `json:"input_ref"`
State string `json:"state"`
ErrorCodes []string `json:"error_codes"`
Metrics metrics `json:"metrics"`
}
var uuid4 = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
var errCode = regexp.MustCompile(`^[A-Z][A-Z0-9_]{2,63}$`)
func parseRuntimeStatus(raw []byte) (runtimeStatus, error) {
var s runtimeStatus
d := json.NewDecoder(bytes.NewReader(raw))
d.DisallowUnknownFields()
if err := d.Decode(&s); err != nil {
return s, errors.New("CONFIG_INVALID")
}
if err := d.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return s, errors.New("CONFIG_INVALID")
}
if s.SchemaVersion != RuntimeStatusVersion {
return s, errors.New("UNSUPPORTED_SCHEMA_VERSION")
}
if !uuid4.MatchString(s.StatusID) || !stableID.MatchString(s.BrainInstanceRef) || s.Sequence < 0 || s.ObservedAt.IsZero() || !validState(s.Runtime.State) || s.Runtime.Version == "" || !stableID.MatchString(s.Model.ModelRef) || s.Model.Version == "" || len(s.Configurations) > 4096 || len(s.Inputs) > 4096 || !validHealth(s.Health) {
return s, errors.New("CONFIG_INVALID")
}
seen := map[string]bool{}
for _, c := range s.Configurations {
if !stableID.MatchString(c.ConfigID) || seen[c.ConfigID] || !validApply(c) {
return s, errors.New("CONFIG_INVALID")
}
seen[c.ConfigID] = true
}
for _, i := range s.Inputs {
if !stableID.MatchString(i.InputRef) || !validState(i.State) || !validMetrics(i.Metrics) || !validCodes(i.ErrorCodes, 16) {
return s, errors.New("CONFIG_INVALID")
}
}
return s, nil
}
func validState(v string) bool {
switch v {
case "unconfigured", "starting", "running", "degraded", "failed", "stopped":
return true
}
return false
}
func validApply(c configurationStatus) bool {
switch c.ApplyState {
case "not_configured":
return c.AppliedRevision == nil
case "applying":
return true
case "applied":
return c.AppliedRevision != nil && *c.AppliedRevision >= 1
case "rejected":
return c.ErrorCode != nil && errCode.MatchString(*c.ErrorCode)
}
return false
}
func validMetrics(m metrics) bool {
return m.LoadPercent >= 0 && m.LoadPercent <= 100 && m.QueueDepth >= 0 && m.LatencyMS >= 0
}
func validCodes(v []string, n int) bool {
if len(v) > n {
return false
}
seen := map[string]bool{}
for _, x := range v {
if seen[x] || !errCode.MatchString(x) {
return false
}
seen[x] = true
}
return true
}
func validHealth(h healthStatus) bool {
return (h.Overall == "healthy" || h.Overall == "degraded" || h.Overall == "unhealthy") && validCodes(h.ErrorCodes, 32) && validMetrics(h.Metrics)
}
func validRuntimeTransition(from, to string) bool {
if from == to {
return true
}
switch from {
case "unconfigured":
return to == "starting" || to == "stopped"
case "starting":
return to == "running" || to == "degraded" || to == "failed" || to == "stopped"
case "running":
return to == "degraded" || to == "failed" || to == "stopped"
case "degraded":
return to == "running" || to == "failed" || to == "stopped"
case "failed":
return to == "starting" || to == "stopped"
case "stopped":
return to == "starting"
}
return false
}
@@ -0,0 +1,159 @@
package brain_control
import (
"encoding/json"
"errors"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type GORMReplayStore struct{ DB *gorm.DB }
func (s GORMReplayStore) Consume(principal, tokenID string, expiresAt, now time.Time) bool {
if s.DB == nil || principal == "" || tokenID == "" || !expiresAt.After(now) {
return false
}
return s.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("expires_at <= ?", now).Delete(&ReplayToken{}).Error; err != nil {
return err
}
result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&ReplayToken{Principal: principal, TokenID: tokenID, ExpiresAt: expiresAt, CreatedAt: now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return errors.New("replayed")
}
return nil
}) == nil
}
type RevisionStore struct{ DB *gorm.DB }
func (s RevisionStore) Next(configID string) (int64, error) {
if s.DB == nil || !stableID.MatchString(configID) {
return 0, errors.New("invalid revision store")
}
var next int64
err := s.DB.Transaction(func(tx *gorm.DB) error {
var row SourceRevision
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("config_id = ?", configID).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
row = SourceRevision{ConfigID: configID, Revision: 1}
if err = tx.Create(&row).Error; err != nil {
return err
}
next = 1
return nil
}
if err != nil {
return err
}
row.Revision++
next = row.Revision
return tx.Save(&row).Error
})
return next, err
}
type ProjectionStore struct {
DB *gorm.DB
Clock func() time.Time
StaleAfter time.Duration
FutureSkew time.Duration
}
type ProjectionView struct {
Projection RuntimeProjection
Offline, Stale, Recovered, RevisionMismatch bool
}
func (s ProjectionStore) Ingest(raw []byte, expected map[string]int64) (ProjectionView, error) {
if s.DB == nil {
return ProjectionView{}, errors.New("projection database required")
}
now := time.Now().UTC()
if s.Clock != nil {
now = s.Clock().UTC()
}
if s.StaleAfter == 0 {
s.StaleAfter = 90 * time.Second
}
if s.FutureSkew == 0 {
s.FutureSkew = 30 * time.Second
}
status, err := parseRuntimeStatus(raw)
if err != nil {
return ProjectionView{}, err
}
if status.ObservedAt.After(now.Add(s.FutureSkew)) {
return ProjectionView{}, errors.New("FUTURE_OBSERVATION")
}
var view ProjectionView
err = s.DB.Transaction(func(tx *gorm.DB) error {
var old RuntimeProjection
find := tx.Where("brain_instance_ref = ?", status.BrainInstanceRef).First(&old).Error
if find == nil {
if old.StatusID == status.StatusID {
view.Projection = old
return nil
}
if status.Sequence <= old.Sequence {
return errors.New("OUT_OF_ORDER_STATUS")
}
if !validRuntimeTransition(old.RuntimeState, status.Runtime.State) {
return errors.New("INVALID_STATUS_TRANSITION")
}
}
if find != nil && !errors.Is(find, gorm.ErrRecordNotFound) {
return find
}
expectedJSON, _ := json.Marshal(expected)
configs, _ := json.Marshal(status.Configurations)
health, _ := json.Marshal(status.Health)
inputs, _ := json.Marshal(status.Inputs)
p := RuntimeProjection{BrainInstanceRef: status.BrainInstanceRef, StatusID: status.StatusID, Sequence: status.Sequence, ObservedAt: status.ObservedAt, ReceivedAt: now, RuntimeState: status.Runtime.State, RuntimeVersion: status.Runtime.Version, ModelRef: status.Model.ModelRef, ModelVersion: status.Model.Version, HealthOverall: status.Health.Overall, ExpectedRevisionsJSON: string(expectedJSON), ConfigurationsJSON: string(configs), HealthJSON: string(health), InputsJSON: string(inputs), WasOffline: find == nil && now.Sub(old.ObservedAt) > s.StaleAfter}
if find == nil {
p.CreatedAt = old.CreatedAt
view.Recovered = p.WasOffline
}
if err := tx.Save(&p).Error; err != nil {
return err
}
view.Projection = p
return nil
})
if err != nil {
return ProjectionView{}, err
}
view.Stale = now.Sub(view.Projection.ObservedAt) > s.StaleAfter
view.Offline = view.Stale
applied := make(map[string]*int64, len(status.Configurations))
for _, c := range status.Configurations {
applied[c.ConfigID] = c.AppliedRevision
}
for configID, want := range expected {
got, ok := applied[configID]
if !ok || got == nil || *got != want {
view.RevisionMismatch = true
}
}
return view, nil
}
func (s ProjectionStore) View(instance string) (ProjectionView, error) {
var p RuntimeProjection
if err := s.DB.First(&p, "brain_instance_ref = ?", instance).Error; err != nil {
return ProjectionView{}, err
}
now := time.Now().UTC()
if s.Clock != nil {
now = s.Clock().UTC()
}
stale := s.StaleAfter
if stale == 0 {
stale = 90 * time.Second
}
return ProjectionView{Projection: p, Offline: now.Sub(p.ObservedAt) > stale, Stale: now.Sub(p.ObservedAt) > stale}, nil
}
@@ -0,0 +1,25 @@
package version
import (
"runtime"
"gorm.io/gorm"
"git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/brain_control"
"git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration"
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
)
func init() {
_, fileName, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateBrainRuntime)
}
func migrateBrainRuntime(db *gorm.DB, version string) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&brain_control.ReplayToken{}, &brain_control.RuntimeProjection{}, &brain_control.SourceRevision{}); err != nil {
return err
}
return tx.Create(&common.Migration{Version: version}).Error
})
}
@@ -0,0 +1,28 @@
package version
import (
"testing"
"git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/brain_control"
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestMigrateBrainRuntime(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:brain-runtime-migration?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&common.Migration{}); err != nil {
t.Fatal(err)
}
if err = migrateBrainRuntime(db, "2026083112000"); err != nil {
t.Fatal(err)
}
for _, model := range []any{&brain_control.ReplayToken{}, &brain_control.RuntimeProjection{}, &brain_control.SourceRevision{}} {
if !db.Migrator().HasTable(model) {
t.Fatalf("missing table for %T", model)
}
}
}
@@ -0,0 +1,138 @@
package brain_control_test
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"strings"
"sync"
"testing"
"time"
bc "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/brain_control"
mi "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/machine_identity"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func testDB(t *testing.T, name string) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+name+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&bc.ReplayToken{}, &bc.RuntimeProjection{}, &bc.SourceRevision{}); err != nil {
t.Fatal(err)
}
return db
}
func TestMapperProducesCredentialFreeFrozenContract(t *testing.T) {
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
c, err := bc.MapSourceConfig(bc.SourceFacts{ConfigID: "gate-primary", SiteID: "site-east", LogicalDeviceID: "camera-1", MediaPath: "site-east/camera-1/main", Revision: 1, PublishedAt: now, EffectiveAt: now, Profile: bc.Profile{ID: "main", Width: 1920, Height: 1080, Encoding: "h264", FrameRate: 25}, RuleSetVersion: "rules-1", Areas: []bc.AreaRule{{ID: "danger", Version: 1, Kind: "danger_area", Enabled: true, Points: []bc.Point{{X: .1, Y: .1}, {X: .8, Y: .1}, {X: .5, Y: .8}}}}})
if err != nil {
t.Fatal(err)
}
if err = bc.ValidateSourceConfig(c); err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(c)
text := strings.ToLower(string(raw))
for _, secret := range []string{"password", "username", "rtsp://", "stream_uri", "credential"} {
if strings.Contains(text, secret) {
t.Fatalf("leaked %q", secret)
}
}
c2, err := bc.MapSourceConfig(bc.SourceFacts{ConfigID: "gate-primary", SiteID: "site-east", LogicalDeviceID: "camera-1", MediaPath: "site-east/camera-1/main", Revision: 2, PublishedAt: now, EffectiveAt: now, Profile: bc.Profile{ID: "main-v2", Width: 1280, Height: 720, Encoding: "H265", FrameRate: 20}, RuleSetVersion: "rules-2", NeedsRecalibration: true, Areas: []bc.AreaRule{{ID: "danger", Version: 2, Kind: "danger_area", Enabled: true, Points: []bc.Point{{X: .1, Y: .1}, {X: .8, Y: .1}, {X: .5, Y: .8}}}}})
if err != nil {
t.Fatal(err)
}
if c2.RuleSet.State != "recalibration_required" || c2.RuleSet.Areas[0].Enabled {
t.Fatal("recalibration must disable rules")
}
}
func TestDurableReplayIsAtomicAndSurvivesVerifierRestart(t *testing.T) {
db := testDB(t, "sense-replay")
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
registry, _ := mi.NewRegistry(mi.KeyRecord{Principal: "yv:brain:east", KeyID: "brain-key-01", PublicKey: pub, Audience: "yovision-sense", Scopes: []string{"runtime-status:write"}, Enabled: true})
signer := mi.Signer{Principal: "yv:brain:east", KeyID: "brain-key-01", PrivateKey: priv, Now: func() time.Time { return now }}
body := []byte(`{"ok":true}`)
token, _ := signer.Mint("yovision-sense", []string{"runtime-status:write"}, "POST", "/machine/v1/runtime-status", body)
results := make(chan bool, 8)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
v := mi.Verifier{Registry: registry, Replay: bc.GORMReplayStore{DB: db}, Now: func() time.Time { return now }}
_, err := v.Verify(token, "yovision-sense", "runtime-status:write", "POST", "/machine/v1/runtime-status", body)
results <- err == nil
}()
}
wg.Wait()
close(results)
accepted := 0
for ok := range results {
if ok {
accepted++
}
}
if accepted != 1 {
t.Fatalf("accepted=%d", accepted)
}
v2 := mi.Verifier{Registry: registry, Replay: bc.GORMReplayStore{DB: db}, Now: func() time.Time { return now }}
if _, err := v2.Verify(token, "yovision-sense", "runtime-status:write", "POST", "/machine/v1/runtime-status", body); err == nil {
t.Fatal("replay accepted after verifier restart")
}
}
func TestRevisionStoreConcurrentAndRestart(t *testing.T) {
db := testDB(t, "sense-revisions")
store := bc.RevisionStore{DB: db}
for want := int64(1); want <= 3; want++ {
got, err := store.Next("gate-primary")
if err != nil || got != want {
t.Fatalf("got %d err %v", got, err)
}
}
restarted := bc.RevisionStore{DB: db}
got, err := restarted.Next("gate-primary")
if err != nil || got != 4 {
t.Fatalf("restart got %d err %v", got, err)
}
}
func TestRuntimeProjectionStaleRecoveryMismatchAndOrdering(t *testing.T) {
db := testDB(t, "sense-projection")
now := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
store := bc.ProjectionStore{DB: db, Clock: func() time.Time { return now }, StaleAfter: 90 * time.Second}
raw := statusJSON("018f4d6a-8d1b-4a25-8b37-9085f9c0d101", 41, now, "running", 20)
view, err := store.Ingest(raw, map[string]int64{"gate-primary": 21})
if err != nil {
t.Fatal(err)
}
if !view.RevisionMismatch || view.Stale {
t.Fatalf("bad initial view %+v", view)
}
now = now.Add(91 * time.Second)
view, err = store.View("brain-east-01")
if err != nil || !view.Offline || !view.Stale {
t.Fatalf("offline %+v %v", view, err)
}
raw = statusJSON("018f4d6a-8d1b-4a25-8b37-9085f9c0d102", 42, now, "running", 21)
view, err = store.Ingest(raw, map[string]int64{"gate-primary": 21})
if err != nil || !view.Recovered || view.RevisionMismatch {
t.Fatalf("recovery %+v %v", view, err)
}
if _, err = store.Ingest(statusJSON("018f4d6a-8d1b-4a25-8b37-9085f9c0d103", 41, now, "running", 21), nil); err == nil || err.Error() != "OUT_OF_ORDER_STATUS" {
t.Fatalf("expected ordering rejection: %v", err)
}
}
func statusJSON(id string, seq int64, observed time.Time, state string, revision int64) []byte {
v := map[string]any{"schema_version": bc.RuntimeStatusVersion, "status_id": id, "brain_instance_ref": "brain-east-01", "sequence": seq, "observed_at": observed.Format(time.RFC3339), "runtime": map[string]any{"state": state, "version": "1.0.0", "started_at": observed.Add(-time.Minute).Format(time.RFC3339)}, "model": map[string]any{"model_ref": "people-detection", "version": "2026.08.1"}, "configurations": []any{map[string]any{"config_id": "gate-primary", "apply_state": "applied", "applied_revision": revision, "error_code": nil}}, "health": map[string]any{"overall": "healthy", "error_codes": []any{}, "metrics": map[string]any{"load_percent": 1.0, "queue_depth": 0, "latency_ms": 2.0}}, "inputs": []any{}}
raw, _ := json.Marshal(v)
return raw
}
@@ -0,0 +1,18 @@
module git.ilapage.cn/ila/yovision/Sense/tests/integration/brain_control
go 1.26.5
require (
git.ilapage.cn/ila/yovision/Sense/server v0.0.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.2
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect
golang.org/x/text v0.40.0 // indirect
)
replace git.ilapage.cn/ila/yovision/Sense/server => ../../../server
@@ -0,0 +1,12 @@
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+12 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Architecture-and-Code-Map
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Architecture-and-Code-Map.-
wiki_revision: 812e822990d8c8e82445bd19ced67aca8c10aba4
synchronized_at: 2026-08-31T01:58:55Z
wiki_revision: 41c2193b2f1edb37abe1e8994d65d02207549039
synchronized_at: 2026-08-31T03:18:12Z
<!-- gitea-wiki-mirror:end -->
# 架构与代码地图
@@ -344,3 +344,13 @@ Brain internal candidate / Sense local event
Brain candidate、Sense candidate/Outbox 与 Bell Event/Receipt/Alert 继续是各自内部模型。当前没有新增可运行的跨端 ingress/relay,不能把冻结 Schema 解释为端到端链路已完成。
<!-- standard-event-evidence-v1:end -->
<!-- machine-identity-v1:start -->
## 三项目机器身份与安全传输 v1
工单 #151 已于 2026-08-31 验收并通过 PR #162 合入 `dev`。共享事实源为 `contracts/machine-identity/v1/**` 与 `contracts/transport/v1/**`;Sense、Brain、Bell 分别在自己的 `integration/machine_identity/` 中保留独立适配,不共享用户、JWT、Cookie、Casbin、数据库或业务实现。
v1 使用 HTTPS 上的 Ed25519 短期请求绑定 JWS。每个部署实例拥有独立 principal、`kid` 和仓库外私钥;消费者使用本地外部公钥注册表,按精确 audience 与最小 scope 授权。令牌绑定 HTTP 方法、规范化路径和正文 SHA-256,有效期最多 300 秒、时钟偏差最多 30 秒,并以 `(principal,jti)` 原子防重放。TLS 最低 1.2,证书链与主机名验证不可关闭。
Sense/Bell 使用 Go 标准库 Ed25519,Brain 冻结 `cryptography==50.0.1`。固定跨语言向量证明 Go/Python 可互相验签。#151 只提供身份、注册表、传输策略及可注入 replay 接口;#152/#153 才注册业务 endpoint,并必须使用各产品独立的持久原子 replay store验证重启,不能共享数据库。
<!-- machine-identity-v1:end -->
+14 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Business-Rules-and-Glossary
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Business-Rules-and-Glossary.-
wiki_revision: bc4a1a7be268028fa85717b71f48f7dd75cc7e52
synchronized_at: 2026-08-31T01:59:04Z
wiki_revision: 7a91edf3ca35ade3e254937c4a68b53d816a3eea
synchronized_at: 2026-08-31T03:18:24Z
<!-- gitea-wiki-mirror:end -->
# 业务规则与术语
@@ -262,3 +262,15 @@ synchronized_at: 2026-08-31T01:59:04Z
- **Bell 所有权**:Bell 独占内部 Event/Receipt、规则、Alert、ack、close、通知与用户审计;上游不得写入这些状态。
- **兼容与回退**:未知主版本终止接收但保留已有事实;破坏性变化发布新主版本。回退停用新生产者版本,不删除 Outbox、Receipt、Event 或审计。
<!-- standard-event-evidence-v1:end -->
<!-- machine-identity-v1:start -->
## 机器身份、权限和重放规则
- 机器 principal 格式为 `yv:<sense|brain|bell>:<instance>`,每个产品实例独立;不得替代、携带或映射 Sense/Bell 用户身份。
- v1 audience 只允许 `yovision-sense`、`yovision-brain`、`yovision-bell`;scope 只允许 `source-config:write`、`runtime-status:write`、`events:ingest`、`evidence:read`,没有通配符。
- 机器令牌只能从 Authorization Bearer 读取,不接受管理员密码、浏览器 JWT/Cookie、query token、共享 secret 或其他产品用户身份。
- 相同 `(principal,jti)` 只能成功一次;传输重试必须签发新令牌和 jti,但业务幂等键与载荷保持不变。
- 私钥只存在于仓库外受操作系统保护的文件或秘密存储;运行配置只引用路径。公钥注册表属于各消费者本地配置,不是共享数据库。
- 轮换先登记新 `kid`,最多并存 24 小时,切换后移除旧 key;禁用 principal 或吊销 `kid` 对每次请求即时生效。
- 回退只能关闭 connector 并恢复三端独立运行,不得降级为明文、共享管理员身份、共享 JWT 或跳过签名/TLS 验证。
<!-- machine-identity-v1:end -->
+29 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Local-Development-and-Verification
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.-
wiki_revision: d11757b202117e028878802e1e8a9ba9df1a8e89
synchronized_at: 2026-08-31T01:59:13Z
wiki_revision: 771fdd92eb03e9dff7d3ae20b7cb89f1ca288959
synchronized_at: 2026-08-31T03:18:34Z
<!-- gitea-wiki-mirror:end -->
# 本地开发与验证
@@ -679,3 +679,30 @@ git diff --check
这些测试只验证冻结契约,不验证 #153 产品 mapper/relay/ingress、#151 机器身份、实际证据存储/授权、网络断线补投或跨项目 E2E。
<!-- standard-event-evidence-v1:end -->
<!-- machine-identity-v1:start -->
## 机器身份与安全传输 v1 验证
从仓库根目录执行隔离契约测试:
```powershell
pwsh -NoProfile -File contracts/tests/machine-identity-v1/run.ps1
```
脚本在系统临时目录建立虚拟环境,按固定 `cryptography==50.0.1` 和 `jsonschema==4.25.1` 验证封闭 Schema、Go/Python 固定 Ed25519 向量、请求绑定、Bearer-only、错 audience/scope、过期、重放、轮换、吊销和 TLS policy,结束后删除所属临时目录。
三端定向验证:
```powershell
cd Sense/server
go test -race ./app/sense/integration/machine_identity
cd ../../Bell/server
go test -race ./app/bell/integration/machine_identity
cd ../..
Brain\.venv\Scripts\python.exe contracts\tests\machine-identity-v1\test_contract.py
```
这些测试只证明 #151 身份和传输基础。真实客户 PKI/网络、现场时钟漂移、业务 endpoint、断网补投以及持久 replay 重启恢复由 #152/#153/#155 验证;不得用进程内 replay store替代生产结论。
<!-- machine-identity-v1:end -->
+17 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Troubleshooting
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting
wiki_revision: 046624c1b2d7963ef733b4c9687d5dec14771388
synchronized_at: 2026-08-27T09:05:32Z
wiki_revision: 75bf70f375f58356e968b45139b54fcae391d1e8
synchronized_at: 2026-08-31T03:18:53Z
<!-- gitea-wiki-mirror:end -->
# 故障排查
@@ -161,3 +161,18 @@ synchronized_at: 2026-08-27T09:05:32Z
3. 若同步提示本地镜像有未提交修改,先核对改动归属并停止覆盖。
4. Wiki 页面缺失、没有 revision、MCP/API 凭据不可用或映射准备删除/重命名时停止,由工单确认后处理。
5. PowerShell 显示乱码时先区分文件编码和控制台输出编码,不默认另起 PowerShell 或使用 `-ExecutionPolicy Bypass`。
<!-- machine-identity-v1:start -->
## 机器身份 v1 排错
| 现象/错误码 | 常见原因 | 检查 | 安全处理 |
|---|---|---|---|
| `machine_token_missing` | 未使用 Authorization Bearer 或格式错误 | 检查 connector 配置,不粘贴令牌 | 修复调用方;不改用 Cookie/query token |
| `machine_token_invalid` | 签名、kid、请求方法/路径/正文摘要或封闭字段不匹配 | 核对版本、kid 和请求绑定 | 停止重试,修复配置/实现 |
| `machine_token_expired` | 时钟偏差或令牌超过 5 分钟 | 检查双方 UTC 时钟 | 校时并签发新令牌,不延长长期 token |
| `machine_audience_denied` / `machine_scope_denied` | 目标产品或最小权限不匹配 | 核对公钥注册表和调用方向 | 修正精确授权,不添加通配符 |
| `machine_identity_revoked` | principal/key 已禁用或吊销 | 核对当前 kid 与轮换状态 | 切换已批准新 key;不得恢复旧 key绕过吊销 |
| `machine_token_replayed` | 同一 jti 被再次使用 | 检查重试是否重新签名 | 新建 jti,保留业务幂等键 |
排错日志只记录稳定错误码、已认证 principal/kid 和 correlation ID;不得记录令牌、签名、密钥、完整 Authorization header 或秘密路径。无法安全恢复时关闭 connector,三端保持独立运行。
<!-- machine-identity-v1:end -->
+14 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Deployment-and-Operations
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Deployment-and-Operations.-
wiki_revision: 0a772b0511044d98430ebd93304faa3dee57183d
synchronized_at: 2026-08-31T01:39:35Z
wiki_revision: 54d720aa07d2c40684da3fc04378393c7f318a04
synchronized_at: 2026-08-31T03:20:15Z
<!-- gitea-wiki-mirror:end -->
# YoVision 部署与运维
@@ -142,3 +142,15 @@ Sense\start_sense.bat
因此现阶段部署仍按 Sense、Brain 各自独立入口进行,不得手工共享数据库、用户 JWT/Cookie、摄像头凭据、文件目录或临时 JSON 字段来提前打通。需要停用或回退时保持两端独立运行,并保留上一已确认的配置与最后已知状态;未知协议主版本必须停止摄取而不是覆盖投影。
<!-- sense-brain-contracts-v1:end -->
<!-- machine-identity-v1:start -->
## 机器身份部署与轮换边界
#151 已冻结机器身份和传输基础,但 #152/#153 尚未注册业务 connector,因此当前不得手工拼接 endpoint 或临时共享凭据提前打通。
部署时为每个调用实例独立生成 Ed25519 私钥,保存到仓库外受 ACL/秘密存储保护的位置;产品配置只记录私钥路径、principal、kid、目标 audience 和最小 scope。消费者从仓库外公钥注册表读取受信 principal/kid。不得把私钥、完整令牌、Authorization header、管理员密码、浏览器 JWT/Cookie 或 query token写入配置样例、日志、工单和备份。
传输固定使用 HTTPS,TLS 最低 1.2并验证证书链与主机名。轮换按“先登记新公钥 → 调用方切换新 kid → 验证流量 → 24 小时内移除旧 key”执行;应急吊销直接禁用 principal 或 key,并同时停用相关 connector。回退保持 Sense、Brain、Bell 独立运行,保留 Outbox、Receipt、Event 和最后已知状态。
#152/#153 必须为各产品注入自己的持久原子 `(principal,jti)` replay store并验证重启;内存 replay store 只用于适配测试或不重启的单进程原语。
<!-- machine-identity-v1:end -->