457 lines
19 KiB
Python
457 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import datetime as dt
|
|
import hashlib
|
|
import json
|
|
import sqlite3
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
UTC = dt.timezone.utc
|
|
_ALLOWED_CONFIDENTIALITY = frozenset({"public", "tenant", "project", "private", "run"})
|
|
_PRIVILEGED = frozenset({"*", "admin", "system.admin", "message.admin", "task.admin"})
|
|
|
|
|
|
class HandoffError(RuntimeError):
|
|
code = "HANDOFF_ERROR"
|
|
|
|
|
|
class MessageScopeMismatch(HandoffError):
|
|
code = "MESSAGE_SCOPE_MISMATCH_DENY"
|
|
|
|
|
|
class ConfidentialityScopeDenied(HandoffError):
|
|
code = "MESSAGE_CONFIDENTIALITY_SCOPE_ENFORCE"
|
|
|
|
|
|
class MessageExpired(HandoffError):
|
|
code = "MESSAGE_EXPIRED"
|
|
|
|
|
|
class SequenceGap(HandoffError):
|
|
code = "MESSAGE_SEQUENCE_GAP_DETECT"
|
|
|
|
|
|
class SequenceConflict(HandoffError):
|
|
code = "MESSAGE_SEQUENCE_CONFLICT"
|
|
|
|
|
|
class IdempotencyConflict(HandoffError):
|
|
code = "MESSAGE_IDEMPOTENCY_CONFLICT"
|
|
|
|
|
|
def _utcnow() -> dt.datetime:
|
|
return dt.datetime.now(UTC)
|
|
|
|
|
|
def _iso(value: dt.datetime | str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) if isinstance(value, str) else value
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=UTC)
|
|
return parsed.astimezone(UTC).isoformat()
|
|
|
|
|
|
def _canonical(value: Any) -> str:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
|
|
|
|
|
|
def _sha(value: Any) -> str:
|
|
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _clean(value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
text = str(value).strip()
|
|
return text or None
|
|
|
|
|
|
def _tuple_scope(value: Iterable[str] | str | None) -> tuple[str, ...]:
|
|
if value is None:
|
|
return ()
|
|
if isinstance(value, str):
|
|
value = value.split(",")
|
|
return tuple(sorted({str(item).strip() for item in value if str(item).strip()}))
|
|
|
|
|
|
_SECRET_KEY = re.compile(r"(?i)(?:token|secret|password|private[_-]?key|api[_-]?key)")
|
|
_SECRET_VALUE = re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/-]{8,}|(?:token|secret|password|api[_-]?key)\s*[:=]\s*\S+")
|
|
_SAFE_REFERENCE = re.compile(r"^(?:systemd-credential|vault|secret-manager):[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$")
|
|
|
|
|
|
def assert_payload_secret_safe(value: Any, *, key: str = "") -> None:
|
|
if isinstance(value, Mapping):
|
|
for child_key, child in value.items():
|
|
assert_payload_secret_safe(child, key=str(child_key))
|
|
return
|
|
if isinstance(value, (list, tuple)):
|
|
for child in value:
|
|
assert_payload_secret_safe(child, key=key)
|
|
return
|
|
if isinstance(value, (bytes, bytearray)):
|
|
raise ValueError("CREDENTIAL_SECRET_EXPOSURE_DENY")
|
|
if isinstance(value, str):
|
|
if _SECRET_VALUE.search(value):
|
|
raise ValueError("CREDENTIAL_SECRET_EXPOSURE_DENY")
|
|
if _SECRET_KEY.search(key) and value and _SAFE_REFERENCE.fullmatch(value) is None:
|
|
raise ValueError("CREDENTIAL_SECRET_EXPOSURE_DENY")
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class DeliveryContext:
|
|
tenant_id: str | None = None
|
|
user_id: str | None = None
|
|
project_id: str | None = None
|
|
conversation_id: str | None = None
|
|
run_id: str | None = None
|
|
capabilities: tuple[str, ...] = ()
|
|
|
|
@classmethod
|
|
def from_actor(cls, actor: Any) -> "DeliveryContext":
|
|
def get(name: str) -> Any:
|
|
if isinstance(actor, Mapping):
|
|
return actor.get(name)
|
|
return getattr(actor, name, None)
|
|
return cls(
|
|
tenant_id=_clean(get("tenant_id")),
|
|
user_id=_clean(get("user_id")),
|
|
project_id=_clean(get("project_id")),
|
|
conversation_id=_clean(get("conversation_id")),
|
|
run_id=_clean(get("run_id")),
|
|
capabilities=_tuple_scope(get("capabilities") or get("capability_scope")),
|
|
)
|
|
|
|
@property
|
|
def privileged(self) -> bool:
|
|
return bool(set(self.capabilities) & _PRIVILEGED)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class MessageEnvelope:
|
|
source: str
|
|
target: str
|
|
message_type: str
|
|
payload: Mapping[str, Any]
|
|
sequence: int
|
|
idempotency_key: str
|
|
tenant_id: str | None = None
|
|
user_id: str | None = None
|
|
project_id: str | None = None
|
|
conversation_id: str | None = None
|
|
task_id: str | None = None
|
|
run_id: str | None = None
|
|
correlation_id: str | None = None
|
|
causation_id: str | None = None
|
|
capability_scope: tuple[str, ...] = ()
|
|
confidentiality_scope: str = "private"
|
|
expires_at: str | None = None
|
|
created_at: str = dataclasses.field(default_factory=lambda: _iso(_utcnow()) or "")
|
|
envelope_id: str = ""
|
|
|
|
def __post_init__(self) -> None:
|
|
assert_payload_secret_safe(self.payload)
|
|
for field in ("source", "target", "message_type", "idempotency_key", "tenant_id", "user_id",
|
|
"project_id", "conversation_id", "task_id", "run_id", "correlation_id", "causation_id"):
|
|
object.__setattr__(self, field, _clean(getattr(self, field)) if field not in {
|
|
"source", "target", "message_type", "idempotency_key"
|
|
} else (_clean(getattr(self, field)) or ""))
|
|
object.__setattr__(self, "capability_scope", _tuple_scope(self.capability_scope))
|
|
object.__setattr__(self, "confidentiality_scope", (_clean(self.confidentiality_scope) or "private").lower())
|
|
object.__setattr__(self, "expires_at", _iso(self.expires_at))
|
|
object.__setattr__(self, "created_at", _iso(self.created_at) or "")
|
|
if not self.source or not self.target or not self.message_type or not self.idempotency_key:
|
|
raise ValueError("MESSAGE_ENVELOPE_IDENTITY_REQUIRED")
|
|
if not isinstance(self.sequence, int) or self.sequence < 1:
|
|
raise ValueError("MESSAGE_SEQUENCE_MUST_BE_POSITIVE")
|
|
if self.confidentiality_scope not in _ALLOWED_CONFIDENTIALITY:
|
|
raise ValueError("MESSAGE_CONFIDENTIALITY_SCOPE_INVALID")
|
|
if self.confidentiality_scope != "public" and self.tenant_id is None:
|
|
raise ValueError("MESSAGE_TENANT_REQUIRED")
|
|
if self.confidentiality_scope in {"project", "private", "run"} and self.project_id is None:
|
|
raise ValueError("MESSAGE_PROJECT_REQUIRED")
|
|
if self.confidentiality_scope in {"private", "run"} and self.user_id is None:
|
|
raise ValueError("MESSAGE_USER_REQUIRED")
|
|
if self.confidentiality_scope == "run" and self.run_id is None:
|
|
raise ValueError("MESSAGE_RUN_REQUIRED")
|
|
expected = stable_event_id(
|
|
source=self.source, target=self.target, message_type=self.message_type, payload=self.payload,
|
|
tenant_id=self.tenant_id, user_id=self.user_id, project_id=self.project_id,
|
|
conversation_id=self.conversation_id, task_id=self.task_id, run_id=self.run_id,
|
|
correlation_id=self.correlation_id, causation_id=self.causation_id,
|
|
sequence=self.sequence, idempotency_key=self.idempotency_key,
|
|
)
|
|
if self.envelope_id and self.envelope_id != expected:
|
|
raise ValueError("MESSAGE_ENVELOPE_ID_MISMATCH")
|
|
object.__setattr__(self, "envelope_id", expected)
|
|
|
|
@property
|
|
def payload_sha256(self) -> str:
|
|
return _sha(self.payload)
|
|
|
|
@property
|
|
def stream_key(self) -> str:
|
|
return _sha({
|
|
"tenant_id": self.tenant_id, "source": self.source, "target": self.target,
|
|
"conversation_id": self.conversation_id, "task_id": self.task_id, "run_id": self.run_id,
|
|
})
|
|
|
|
def is_expired(self, *, now: dt.datetime | None = None) -> bool:
|
|
if self.expires_at is None:
|
|
return False
|
|
return dt.datetime.fromisoformat(self.expires_at) <= (now or _utcnow()).astimezone(UTC)
|
|
|
|
def as_record(self) -> dict[str, Any]:
|
|
return {
|
|
"envelope_id": self.envelope_id, "stream_key": self.stream_key, "source": self.source,
|
|
"target": self.target, "message_type": self.message_type, "payload_json": _canonical(self.payload),
|
|
"payload_sha256": self.payload_sha256, "tenant_id": self.tenant_id, "user_id": self.user_id,
|
|
"project_id": self.project_id, "conversation_id": self.conversation_id, "task_id": self.task_id,
|
|
"run_id": self.run_id, "correlation_id": self.correlation_id,
|
|
"causation_id": self.causation_id, "sequence": self.sequence,
|
|
"capability_scope_json": _canonical(self.capability_scope),
|
|
"confidentiality_scope": self.confidentiality_scope, "idempotency_key": self.idempotency_key,
|
|
"expires_at": self.expires_at, "created_at": self.created_at,
|
|
}
|
|
|
|
|
|
def stable_event_id(*, source: Any, target: Any, message_type: Any, payload: Any,
|
|
tenant_id: Any = None, user_id: Any = None, project_id: Any = None,
|
|
conversation_id: Any = None, task_id: Any = None, run_id: Any = None,
|
|
correlation_id: Any = None, causation_id: Any = None,
|
|
sequence: Any = None, idempotency_key: Any = None) -> str:
|
|
assert_payload_secret_safe(payload)
|
|
material = {
|
|
"source": _clean(source), "target": _clean(target), "message_type": _clean(message_type),
|
|
"payload_sha256": _sha(payload), "tenant_id": _clean(tenant_id), "user_id": _clean(user_id),
|
|
"project_id": _clean(project_id), "conversation_id": _clean(conversation_id),
|
|
"task_id": _clean(task_id), "run_id": _clean(run_id), "causation_id": _clean(causation_id),
|
|
"sequence": sequence, "idempotency_key": _clean(idempotency_key),
|
|
}
|
|
# Preserve historical envelope IDs for legacy rows that predate correlation_id.
|
|
if _clean(correlation_id) is not None:
|
|
material["correlation_id"] = _clean(correlation_id)
|
|
return _sha(material)
|
|
|
|
|
|
def bind_envelope_scope(envelope: MessageEnvelope, actor: Any) -> MessageEnvelope:
|
|
ctx = DeliveryContext.from_actor(actor)
|
|
if ctx.privileged:
|
|
return envelope
|
|
if ctx.tenant_id is None:
|
|
if envelope.tenant_id is not None:
|
|
raise MessageScopeMismatch(MessageScopeMismatch.code)
|
|
return envelope
|
|
updates: dict[str, Any] = {}
|
|
for field in ("tenant_id", "user_id", "project_id", "conversation_id", "run_id"):
|
|
trusted = getattr(ctx, field)
|
|
requested = getattr(envelope, field)
|
|
if requested is not None and requested != trusted:
|
|
raise MessageScopeMismatch(f"{MessageScopeMismatch.code}:{field}")
|
|
if requested is None and trusted is not None:
|
|
updates[field] = trusted
|
|
if not updates:
|
|
return envelope
|
|
data = dataclasses.asdict(envelope)
|
|
data.update(updates)
|
|
data["envelope_id"] = ""
|
|
return MessageEnvelope(**data)
|
|
|
|
|
|
def authorize_delivery(actor: Any, envelope: MessageEnvelope) -> None:
|
|
ctx = DeliveryContext.from_actor(actor)
|
|
if ctx.privileged:
|
|
return
|
|
if not set(envelope.capability_scope).issubset(set(ctx.capabilities)):
|
|
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:capability")
|
|
if envelope.confidentiality_scope == "public":
|
|
return
|
|
if ctx.tenant_id != envelope.tenant_id:
|
|
raise MessageScopeMismatch(f"{MessageScopeMismatch.code}:tenant_id")
|
|
if envelope.confidentiality_scope in {"project", "private", "run"} and ctx.project_id != envelope.project_id:
|
|
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:project_id")
|
|
if envelope.confidentiality_scope in {"private", "run"} and ctx.user_id != envelope.user_id:
|
|
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:user_id")
|
|
if envelope.confidentiality_scope == "run" and ctx.run_id != envelope.run_id:
|
|
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:run_id")
|
|
|
|
|
|
_SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS message_handoff (
|
|
envelope_id TEXT PRIMARY KEY,
|
|
stream_key TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
message_type TEXT NOT NULL,
|
|
payload_json TEXT NOT NULL,
|
|
payload_sha256 TEXT NOT NULL,
|
|
tenant_id TEXT,
|
|
user_id TEXT,
|
|
project_id TEXT,
|
|
conversation_id TEXT,
|
|
task_id TEXT,
|
|
run_id TEXT,
|
|
correlation_id TEXT,
|
|
causation_id TEXT,
|
|
sequence INTEGER NOT NULL CHECK(sequence >= 1),
|
|
capability_scope_json TEXT NOT NULL,
|
|
confidentiality_scope TEXT NOT NULL,
|
|
idempotency_key TEXT NOT NULL,
|
|
expires_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
delivered_at TEXT,
|
|
acked_at TEXT,
|
|
UNIQUE(stream_key, idempotency_key),
|
|
UNIQUE(stream_key, sequence)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_message_handoff_target_status
|
|
ON message_handoff(target, status, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_message_handoff_scope
|
|
ON message_handoff(tenant_id, project_id, user_id, conversation_id, run_id);
|
|
"""
|
|
|
|
|
|
class DurableHandoffStore:
|
|
def __init__(self, path: str | Path) -> None:
|
|
self.path = Path(path)
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
with self._connect() as conn:
|
|
conn.executescript(_SCHEMA)
|
|
columns = {str(row[1]) for row in conn.execute("PRAGMA table_info(message_handoff)")}
|
|
if "correlation_id" not in columns:
|
|
conn.execute("ALTER TABLE message_handoff ADD COLUMN correlation_id TEXT")
|
|
conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_message_handoff_lineage "
|
|
"ON message_handoff(correlation_id, causation_id)"
|
|
)
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(self.path, timeout=30.0)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
conn.execute("PRAGMA busy_timeout=30000")
|
|
return conn
|
|
|
|
@staticmethod
|
|
def _from_row(row: sqlite3.Row) -> MessageEnvelope:
|
|
return MessageEnvelope(
|
|
source=row["source"], target=row["target"], message_type=row["message_type"],
|
|
payload=json.loads(row["payload_json"]), tenant_id=row["tenant_id"], user_id=row["user_id"],
|
|
project_id=row["project_id"], conversation_id=row["conversation_id"], task_id=row["task_id"],
|
|
run_id=row["run_id"], correlation_id=row["correlation_id"],
|
|
causation_id=row["causation_id"], sequence=int(row["sequence"]),
|
|
capability_scope=tuple(json.loads(row["capability_scope_json"])),
|
|
confidentiality_scope=row["confidentiality_scope"], idempotency_key=row["idempotency_key"],
|
|
expires_at=row["expires_at"], created_at=row["created_at"], envelope_id=row["envelope_id"],
|
|
)
|
|
|
|
def put(self, envelope: MessageEnvelope) -> tuple[str, MessageEnvelope]:
|
|
if envelope.is_expired():
|
|
raise MessageExpired(MessageExpired.code)
|
|
record = envelope.as_record()
|
|
with self._connect() as conn:
|
|
conn.execute("BEGIN IMMEDIATE")
|
|
replay = conn.execute(
|
|
"SELECT * FROM message_handoff WHERE stream_key=? AND idempotency_key=?",
|
|
(envelope.stream_key, envelope.idempotency_key),
|
|
).fetchone()
|
|
if replay is not None:
|
|
existing = self._from_row(replay)
|
|
if existing.envelope_id != envelope.envelope_id or existing.payload_sha256 != envelope.payload_sha256:
|
|
raise IdempotencyConflict(IdempotencyConflict.code)
|
|
conn.commit()
|
|
return "REPLAY", existing
|
|
seq = conn.execute(
|
|
"SELECT * FROM message_handoff WHERE stream_key=? AND sequence=?",
|
|
(envelope.stream_key, envelope.sequence),
|
|
).fetchone()
|
|
if seq is not None:
|
|
existing = self._from_row(seq)
|
|
if existing.envelope_id == envelope.envelope_id:
|
|
conn.commit()
|
|
return "REPLAY", existing
|
|
raise SequenceConflict(SequenceConflict.code)
|
|
maximum = int(conn.execute(
|
|
"SELECT COALESCE(MAX(sequence),0) FROM message_handoff WHERE stream_key=?",
|
|
(envelope.stream_key,),
|
|
).fetchone()[0])
|
|
expected = maximum + 1
|
|
if envelope.sequence != expected:
|
|
raise SequenceGap(f"{SequenceGap.code}:expected={expected}:actual={envelope.sequence}")
|
|
columns = tuple(record)
|
|
placeholders = ",".join("?" for _ in columns)
|
|
conn.execute(
|
|
f"INSERT INTO message_handoff ({','.join(columns)}) VALUES ({placeholders})",
|
|
tuple(record[column] for column in columns),
|
|
)
|
|
conn.commit()
|
|
return "CREATED", envelope
|
|
|
|
def get(self, envelope_id: str, *, actor: Any) -> MessageEnvelope:
|
|
with self._connect() as conn:
|
|
row = conn.execute("SELECT * FROM message_handoff WHERE envelope_id=?", (envelope_id,)).fetchone()
|
|
if row is None:
|
|
raise KeyError(envelope_id)
|
|
envelope = self._from_row(row)
|
|
if envelope.is_expired():
|
|
conn.execute("UPDATE message_handoff SET status='EXPIRED' WHERE envelope_id=?", (envelope_id,))
|
|
conn.commit()
|
|
raise MessageExpired(MessageExpired.code)
|
|
authorize_delivery(actor, envelope)
|
|
return envelope
|
|
|
|
def pending(self, *, target: str, actor: Any, limit: int = 100) -> list[MessageEnvelope]:
|
|
if not 1 <= limit <= 1000:
|
|
raise ValueError("MESSAGE_PENDING_LIMIT_INVALID")
|
|
visible: list[MessageEnvelope] = []
|
|
with self._connect() as conn:
|
|
rows = conn.execute(
|
|
"SELECT * FROM message_handoff WHERE target=? AND status='PENDING' ORDER BY created_at, sequence LIMIT ?",
|
|
(target, limit),
|
|
).fetchall()
|
|
for row in rows:
|
|
envelope = self._from_row(row)
|
|
if envelope.is_expired():
|
|
conn.execute("UPDATE message_handoff SET status='EXPIRED' WHERE envelope_id=?", (envelope.envelope_id,))
|
|
continue
|
|
try:
|
|
authorize_delivery(actor, envelope)
|
|
except HandoffError:
|
|
continue
|
|
visible.append(envelope)
|
|
conn.commit()
|
|
return visible
|
|
|
|
def mark_delivered(self, envelope_id: str, *, actor: Any) -> MessageEnvelope:
|
|
envelope = self.get(envelope_id, actor=actor)
|
|
with self._connect() as conn:
|
|
conn.execute(
|
|
"UPDATE message_handoff SET status='DELIVERED', delivered_at=? "
|
|
"WHERE envelope_id=? AND status='PENDING'",
|
|
(_iso(_utcnow()), envelope_id),
|
|
)
|
|
conn.commit()
|
|
return envelope
|
|
|
|
def acknowledge(self, envelope_id: str, *, actor: Any) -> MessageEnvelope:
|
|
envelope = self.get(envelope_id, actor=actor)
|
|
with self._connect() as conn:
|
|
conn.execute(
|
|
"UPDATE message_handoff SET status='ACKED', acked_at=? WHERE envelope_id=?",
|
|
(_iso(_utcnow()), envelope_id),
|
|
)
|
|
conn.commit()
|
|
return envelope
|
|
|
|
|
|
class HandoffFabric:
|
|
def __init__(self, store: DurableHandoffStore) -> None:
|
|
self.store = store
|
|
|
|
def publish(self, envelope: MessageEnvelope, *, actor: Any | None = None) -> tuple[str, MessageEnvelope]:
|
|
return self.store.put(bind_envelope_scope(envelope, actor) if actor is not None else envelope)
|