repository.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """In-memory reference behavior for the fixed WP13 fixture dispatcher.
  2. The production persistence boundary is supplied by the WP13 PostgreSQL gateway.
  3. This small adapter exists for deterministic unit contracts; it never loads or
  4. executes a submitted artifact.
  5. """
  6. from __future__ import annotations
  7. import copy
  8. import hashlib
  9. import threading
  10. import uuid
  11. from collections.abc import Callable
  12. from typing import Any
  13. from .governance import (
  14. PluginManifestError,
  15. canonical_digest,
  16. fixture_signature_digest,
  17. normalize_plugin_manifest,
  18. )
  19. class PluginLifecycleError(ValueError):
  20. """A lifecycle transition, approval, registry record or invocation was denied."""
  21. _RECORD = frozenset(
  22. {"artifact_digest", "signature", "sbom_digest", "license_digest", "vulnerability_digest", "provenance_digest"}
  23. )
  24. _SIGNATURE = frozenset({"trust_store_key_id", "signature_digest"})
  25. _TRUST_STORE = frozenset({"local-fixture-key-v1"})
  26. _HEX = frozenset("0123456789abcdef")
  27. _ACTIONS = frozenset({"activate", "pause", "rollback", "revoke", "recover"})
  28. def _digest(value: Any, label: str) -> str:
  29. if not isinstance(value, str) or len(value) != 64 or set(value) - _HEX:
  30. raise PluginLifecycleError(f"{label}_invalid")
  31. return value
  32. def _closed(value: Any, keys: frozenset[str], label: str) -> dict[str, Any]:
  33. if not isinstance(value, dict) or set(value) != keys:
  34. raise PluginLifecycleError(f"{label}_closed")
  35. return copy.deepcopy(value)
  36. def _scope(value: Any, *, principal: bool) -> dict[str, str]:
  37. keys = {"tenant_id", "domain"} | ({"principal_id"} if principal else set())
  38. if not isinstance(value, dict) or set(value) != keys:
  39. raise PluginLifecycleError("scope_closed")
  40. normalized: dict[str, str] = {}
  41. for key in keys:
  42. item = value[key]
  43. if not isinstance(item, str) or not item.isascii() or not 1 <= len(item) <= 120:
  44. raise PluginLifecycleError("scope_invalid")
  45. normalized[key] = item
  46. return normalized
  47. class PluginPlatform:
  48. """Thread-safe local state machine around one fixed evidence fixture."""
  49. def __init__(self, *, now: Callable[[], int]) -> None:
  50. self._now = now
  51. self._lock = threading.RLock()
  52. self._versions: dict[tuple[str, str], dict[str, Any]] = {}
  53. self._approvals: dict[str, dict[str, Any]] = {}
  54. self._runs: dict[tuple[str, str, str, str], dict[str, Any]] = {}
  55. def register(self, manifest: Any, *, registry_record: Any) -> dict[str, Any]:
  56. try:
  57. normalized = normalize_plugin_manifest(manifest)
  58. except PluginManifestError as exc:
  59. raise PluginLifecycleError(str(exc)) from exc
  60. record = _closed(registry_record, _RECORD, "registry_record")
  61. signature = _closed(record["signature"], _SIGNATURE, "signature")
  62. if signature["trust_store_key_id"] not in _TRUST_STORE:
  63. raise PluginLifecycleError("trust_store_key_denied")
  64. normalized_record = {
  65. "artifact_digest": _digest(record["artifact_digest"], "artifact_digest"),
  66. "signature": {
  67. "trust_store_key_id": signature["trust_store_key_id"],
  68. "signature_digest": _digest(signature["signature_digest"], "signature_digest"),
  69. },
  70. "sbom_digest": _digest(record["sbom_digest"], "sbom_digest"),
  71. "license_digest": _digest(record["license_digest"], "license_digest"),
  72. "vulnerability_digest": _digest(record["vulnerability_digest"], "vulnerability_digest"),
  73. "provenance_digest": _digest(record["provenance_digest"], "provenance_digest"),
  74. }
  75. if normalized_record["artifact_digest"] != normalized["distribution"]["artifact_digest"]:
  76. raise PluginLifecycleError("artifact_digest_mismatch")
  77. if normalized_record["signature"]["signature_digest"] != fixture_signature_digest(normalized_record["artifact_digest"]):
  78. raise PluginLifecycleError("signature_binding_denied")
  79. key = (normalized["plugin_uid"], normalized["version"])
  80. with self._lock:
  81. if key in self._versions:
  82. raise PluginLifecycleError("plugin_version_exists")
  83. item = normalized | {"state": "draft", "registry": normalized_record, "reviewer_id": None}
  84. self._versions[key] = item
  85. return copy.deepcopy(item)
  86. def review(self, plugin_uid: str, version: str, *, reviewer_id: str) -> dict[str, Any]:
  87. if not isinstance(reviewer_id, str) or not reviewer_id.isascii() or not reviewer_id:
  88. raise PluginLifecycleError("reviewer_invalid")
  89. with self._lock:
  90. item = self._version(plugin_uid, version)
  91. if item["state"] != "draft":
  92. raise PluginLifecycleError("review_transition_denied")
  93. item["state"] = "reviewed"
  94. item["reviewer_id"] = reviewer_id
  95. return copy.deepcopy(item)
  96. def issue_approval(self, *, plugin_uid: str, version: str, action: str, actor_id: str, reviewer_id: str, scope: Any, expires_at: int) -> dict[str, Any]:
  97. if action not in _ACTIONS or not isinstance(actor_id, str) or not isinstance(reviewer_id, str) or actor_id == reviewer_id:
  98. raise PluginLifecycleError("approval_independence_denied")
  99. if not isinstance(expires_at, int) or expires_at <= self._now():
  100. raise PluginLifecycleError("approval_expiry_invalid")
  101. bound_scope = _scope(scope, principal=False)
  102. with self._lock:
  103. item = self._version(plugin_uid, version)
  104. if (
  105. item["state"] != "reviewed"
  106. or item["reviewer_id"] != reviewer_id
  107. or item["reviewer_id"] == actor_id
  108. ):
  109. raise PluginLifecycleError("approval_review_denied")
  110. approval = {
  111. "approval_id": str(uuid.uuid4()), "plugin_uid": plugin_uid, "version": version,
  112. "action": action, "actor_id": actor_id, "reviewer_id": reviewer_id,
  113. "scope": bound_scope, "manifest_digest": item["manifest_digest"],
  114. "expires_at": expires_at, "consumed": False,
  115. }
  116. self._approvals[approval["approval_id"]] = approval
  117. return copy.deepcopy(approval)
  118. def activate(self, plugin_uid: str, version: str, *, actor_id: str, scope: Any, approval_id: str) -> dict[str, Any]:
  119. bound_scope = _scope(scope, principal=False)
  120. with self._lock:
  121. item = self._version(plugin_uid, version)
  122. approval = self._approvals.get(approval_id)
  123. if not approval or approval["consumed"] or approval["expires_at"] <= self._now() or approval["action"] != "activate" or approval["actor_id"] != actor_id or approval["reviewer_id"] != item["reviewer_id"] or approval["scope"] != bound_scope or approval["manifest_digest"] != item["manifest_digest"]:
  124. raise PluginLifecycleError("approval_binding_denied")
  125. if item["state"] != "reviewed":
  126. raise PluginLifecycleError("activation_transition_denied")
  127. if any(candidate["state"] == "active" for (uid, _), candidate in self._versions.items() if uid == plugin_uid):
  128. raise PluginLifecycleError("plugin_active_collision")
  129. approval["consumed"] = True
  130. item["state"] = "active"
  131. return copy.deepcopy(item)
  132. def invoke(self, *, plugin_uid: str, version: str, operation: str, scope: Any, input_digest: str, idempotency_key: str) -> dict[str, Any]:
  133. invocation_scope = _scope(scope, principal=True)
  134. digest = _digest(input_digest, "input_digest")
  135. if not isinstance(idempotency_key, str) or not idempotency_key.isascii() or not 1 <= len(idempotency_key) <= 120:
  136. raise PluginLifecycleError("idempotency_key_invalid")
  137. with self._lock:
  138. item = self._version(plugin_uid, version)
  139. if item["state"] != "active":
  140. raise PluginLifecycleError("plugin_not_active")
  141. if operation not in item["capabilities"]:
  142. raise PluginLifecycleError("capability_denied")
  143. key = (plugin_uid, version, invocation_scope["tenant_id"], idempotency_key)
  144. replay = self._runs.get(key)
  145. request = {"scope": invocation_scope, "input_digest": digest, "operation": operation}
  146. request_digest = canonical_digest(request)
  147. if replay:
  148. if replay["request_digest"] != request_digest:
  149. raise PluginLifecycleError("idempotency_conflict")
  150. return copy.deepcopy(replay["result"] | {"replay": True})
  151. output_digest = hashlib.sha256(
  152. f"ENGINEERING_EVIDENCE_ONLY|{item['manifest_digest']}|{request_digest}".encode("ascii")
  153. ).hexdigest()
  154. result = {
  155. "plugin_uid": plugin_uid, "version": version, "run_digest": request_digest,
  156. "output_digest": output_digest, "fixture_id": "ENGINEERING_EVIDENCE_ONLY", "replay": False,
  157. }
  158. self._runs[key] = {"request_digest": request_digest, "result": result}
  159. return copy.deepcopy(result)
  160. def _version(self, plugin_uid: str, version: str) -> dict[str, Any]:
  161. item = self._versions.get((plugin_uid, version))
  162. if item is None:
  163. raise PluginLifecycleError("plugin_version_not_found")
  164. return item
  165. __all__ = ["PluginLifecycleError", "PluginPlatform"]